Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
const { recorder, event } = require("codeceptjs");
const Container = require("codeceptjs/lib/container");
const { assert } = require("chai");
const format = require("util").format;
 
const supportedHelpers = ["Playwright"];
 
/**
 * @typedef {boolean|RegExp|RegExp[]} ErrorsFilters
 */
 
/**
 * @typedef {Object} ErrorFiltersConfig
 * @property {ErrorsFilters} [ignore=false] - Should this messages be ignored?
 * @property {ErrorsFilters} [display=false] - Should this messages be displayed?
 * @property {ErrorsFilters} [interrupt=false] - Should this messages throw an exception?
 */
 
/**
 * @typedef {Object} errorsCollectorConfig
 * @property {boolean} [collectErrors=true] - Should uncaught errors be collected?
 * @property {boolean} [collectConsoleErrors=true] - Should console errors be collected?
 * @property {boolean} [collectConsoleWarning=true] - Should console warnings be collected?
 * @property {ErrorFiltersConfig} [filter] - Filter config for all cases
 * @property {ErrorFiltersConfig} [uncaughtErrorFilter] - Filter config for uncaught errors
 * @property {ErrorFiltersConfig} [consoleErrorFilter] - Filter config for console errors
 * @property {ErrorFiltersConfig} [consoleWarningFilter] - Filter config for console warnings
 */
 
const defaultConfig = {
  collectErrors: true,
  collectConsoleErrors: true,
  collectConsoleWarning: true,
  filter: {
    ignore: false,
    display: false,
    interrupt: false,
  },
  uncaughtErrorFilter: {
    // Ignore not meaningful errors
    ignore: [/^ResizeObserver loop limit exceeded$/],
  },
};
 
const UNCAUGHT_ERROR = "uncaughtError";
const CONSOLE_ERROR = "consoleError";
const CONSOLE_WARNING = "consoleWarning";
 
const IGNORE_ACTION = "ignore";
const DISPLAY_ACTION = "display";
const INTERRUPT_ACTION = "interrupt";
 
/**
 * Safely get the jsonValue of a JSHandle, handling "Target closed" and other errors.
 * @param {any} arg - The JSHandle to get the value from
 * @returns {Promise<any>} - The value, or undefined if target closed, or a fallback string on other errors
 */
async function safeJsonValue(arg) {
  try {
    return await arg.jsonValue();
  } catch (error) {
    if (error.message && error.message.includes("Target closed")) return;
    if (error.message && error.message.includes("Target page, context or browser has been closed")) return;
 
    console.error(error);
  }
}
 
/**
 * This plugin can monitor three types of errors inside the browser. They are console errors, console warnings and uncaught errors.
 * Depending on the configuration it could show the problems during the tests and throw exceptions at the scenario level to make the test fail  when it is necessary.
 * @param {errorsCollectorConfig} config
 */
module.exports = (config) => {
  const helpers = Container.helpers();
  // find the first helper which is currently supported
  const helper = helpers[Object.keys(helpers).find((helper) => supportedHelpers.includes(helper))];
 
  if (!helper) {
    console.error(`Errors collector plugin is only supported in ${supportedHelpers.join(", ")}`);
    return;
  }
 
  const options = Object.assign({}, defaultConfig, config);
 
  for (const key of ["filter", "uncaughtErrorFilter", "consoleErrorFilter", "consoleWarningFilter"]) {
    options[key] = Object.assign({}, defaultConfig[key], options[key]);
  }
 
  function ErrorCollector(page, parent) {
    this.parent = parent;
    this.page = page;
    this.run();
  }
 
  ErrorCollector.prototype.testMessage = (filter, message) => {
    if (typeof filter === "boolean") return filter;
    if (filter instanceof RegExp) return filter.test(message);
    if (Array.isArray(filter)) return filter.some((f) => f.test(message));
    return false;
  };
 
  ErrorCollector.prototype.should = function (actionType, messageType, message) {
    return (
      this.testMessage(options.filter?.[actionType], message) ||
      this.testMessage(options[`${messageType}Filter`]?.[actionType], message)
    );
  };
 
  ErrorCollector.prototype.handleMessage = function (type, message) {
    if (this.should(IGNORE_ACTION, type, message)) return;
    if (this.should(INTERRUPT_ACTION, type, message)) {
      this.parent.addError(message);
    }
    if (this.should(DISPLAY_ACTION, type, message)) {
      console.warn(message);
    }
  };
 
  ErrorCollector.prototype.run = function () {
    const { page } = this;
 
    page.on("console", async (msg) => {
      const type = msg.type();
      let messageType;
 
      switch (type) {
        case "error": {
          messageType = CONSOLE_ERROR;
          break;
        }
        case "warning": {
          messageType = CONSOLE_WARNING;
          break;
        }
      }
      if (messageType) {
        const args = msg.args();
 
        for (let i = 0; i < args.length; i++) {
          args[i] = await safeJsonValue(args[i]);
          if (args[i] === undefined) return; // Target closed, skip processing
        }
 
        this.handleMessage(messageType, format(...args));
      }
    });
 
    page.on("pageerror", (exception) => {
      this.handleMessage(UNCAUGHT_ERROR, exception);
    });
  };
 
  function ErrorCollectors() {
    this.errors = [];
    this.collectors = {};
  }
  ErrorCollectors.prototype.addError = function (message) {
    this.errors.push(message);
  };
  ErrorCollectors.prototype.isRunningOn = function (page) {
    return !!this.collectors[page._guid];
  };
  ErrorCollectors.prototype.runErrorsCollectorOn = function (page) {
    this.collectors[page._guid] = new ErrorCollector(page, this);
  };
  ErrorCollectors.prototype.reset = function () {
    this.collectors = {};
    this.errors = [];
  };
 
  const errorCollectors = new ErrorCollectors();
 
  event.dispatcher.on(event.test.before, async () => {
    errorCollectors.reset();
  });
 
  event.dispatcher.on(event.step.before, async (step) => {
    if (step.name === "amOnPage") {
      recorder.add("run collector", async () => {
        try {
          if (!errorCollectors.isRunningOn(step.helper.page)) {
            errorCollectors.runErrorsCollectorOn(step.helper.page);
          }
        } catch (err) {
          console.error(err);
        }
      });
    }
  });
  event.dispatcher.on(event.step.after, async () => {
    recorder.add("check for errors", async () => {
      for (const err of errorCollectors.errors) {
        if (err instanceof Error) {
          assert.fail(err.stack);
        } else {
          assert.fail(err);
        }
      }
    });
  });
};
 
module.exports.defaultConfig = defaultConfig;