fixtureRunner.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.FixtureRunner = void 0;
  6. var _util = require("../util");
  7. var _utils = require("playwright-core/lib/utils");
  8. var _fixtures = require("../common/fixtures");
  9. /**
  10. * Copyright Microsoft Corporation. All rights reserved.
  11. *
  12. * Licensed under the Apache License, Version 2.0 (the "License");
  13. * you may not use this file except in compliance with the License.
  14. * You may obtain a copy of the License at
  15. *
  16. * http://www.apache.org/licenses/LICENSE-2.0
  17. *
  18. * Unless required by applicable law or agreed to in writing, software
  19. * distributed under the License is distributed on an "AS IS" BASIS,
  20. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  21. * See the License for the specific language governing permissions and
  22. * limitations under the License.
  23. */
  24. class Fixture {
  25. constructor(runner, registration) {
  26. this.runner = void 0;
  27. this.registration = void 0;
  28. this.value = void 0;
  29. this.failed = false;
  30. this._useFuncFinished = void 0;
  31. this._selfTeardownComplete = void 0;
  32. this._teardownWithDepsComplete = void 0;
  33. this._runnableDescription = void 0;
  34. this._deps = new Set();
  35. this._usages = new Set();
  36. this.runner = runner;
  37. this.registration = registration;
  38. this.value = null;
  39. const title = this.registration.customTitle || this.registration.name;
  40. this._runnableDescription = {
  41. title,
  42. phase: 'setup',
  43. location: registration.location,
  44. slot: this.registration.timeout === undefined ? undefined : {
  45. timeout: this.registration.timeout,
  46. elapsed: 0
  47. }
  48. };
  49. }
  50. async setup(testInfo) {
  51. if (typeof this.registration.fn !== 'function') {
  52. this.value = this.registration.fn;
  53. return;
  54. }
  55. const params = {};
  56. for (const name of this.registration.deps) {
  57. const registration = this.runner.pool.resolveDependency(this.registration, name);
  58. const dep = await this.runner.setupFixtureForRegistration(registration, testInfo);
  59. // Fixture teardown is root => leafs, when we need to teardown a fixture,
  60. // it recursively tears down its usages first.
  61. dep._usages.add(this);
  62. // Don't forget to decrement all usages when fixture goes.
  63. // Otherwise worker-scope fixtures will retain test-scope fixtures forever.
  64. this._deps.add(dep);
  65. params[name] = dep.value;
  66. if (dep.failed) {
  67. this.failed = true;
  68. return;
  69. }
  70. }
  71. // Break the registration function into before/after steps. Create these before/after stacks
  72. // w/o scopes, and create single mutable step that will be converted into the after step.
  73. const shouldGenerateStep = !this.registration.hideStep && !this.registration.name.startsWith('_') && !this.registration.option;
  74. const isInternalFixture = this.registration.location && (0, _util.filterStackFile)(this.registration.location.file);
  75. let mutableStepOnStack;
  76. let afterStep;
  77. let called = false;
  78. const useFuncStarted = new _utils.ManualPromise();
  79. (0, _util.debugTest)(`setup ${this.registration.name}`);
  80. const useFunc = async value => {
  81. if (called) throw new Error(`Cannot provide fixture value for the second time`);
  82. called = true;
  83. this.value = value;
  84. this._useFuncFinished = new _utils.ManualPromise();
  85. useFuncStarted.resolve();
  86. await this._useFuncFinished;
  87. if (shouldGenerateStep) {
  88. afterStep = testInfo._addStep({
  89. wallTime: Date.now(),
  90. title: `fixture: ${this.registration.name}`,
  91. category: 'fixture',
  92. location: isInternalFixture ? this.registration.location : undefined
  93. }, testInfo._afterHooksStep);
  94. mutableStepOnStack.stepId = afterStep.stepId;
  95. }
  96. };
  97. const workerInfo = {
  98. config: testInfo.config,
  99. parallelIndex: testInfo.parallelIndex,
  100. workerIndex: testInfo.workerIndex,
  101. project: testInfo.project
  102. };
  103. const info = this.registration.scope === 'worker' ? workerInfo : testInfo;
  104. testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
  105. const handleError = e => {
  106. this.failed = true;
  107. if (!useFuncStarted.isDone()) useFuncStarted.reject(e);else throw e;
  108. };
  109. try {
  110. const result = _utils.zones.preserve(async () => {
  111. if (!shouldGenerateStep) return await this.registration.fn(params, useFunc, info);
  112. await testInfo._runAsStep({
  113. title: `fixture: ${this.registration.name}`,
  114. category: 'fixture',
  115. location: isInternalFixture ? this.registration.location : undefined
  116. }, async step => {
  117. mutableStepOnStack = step;
  118. return await this.registration.fn(params, useFunc, info);
  119. });
  120. });
  121. if (result instanceof Promise) this._selfTeardownComplete = result.catch(handleError);else this._selfTeardownComplete = Promise.resolve();
  122. } catch (e) {
  123. handleError(e);
  124. }
  125. await useFuncStarted;
  126. if (shouldGenerateStep) {
  127. var _mutableStepOnStack, _this$_selfTeardownCo;
  128. (_mutableStepOnStack = mutableStepOnStack) === null || _mutableStepOnStack === void 0 ? void 0 : _mutableStepOnStack.complete({});
  129. (_this$_selfTeardownCo = this._selfTeardownComplete) === null || _this$_selfTeardownCo === void 0 ? void 0 : _this$_selfTeardownCo.then(() => {
  130. var _afterStep;
  131. (_afterStep = afterStep) === null || _afterStep === void 0 ? void 0 : _afterStep.complete({});
  132. }).catch(e => {
  133. var _afterStep2;
  134. (_afterStep2 = afterStep) === null || _afterStep2 === void 0 ? void 0 : _afterStep2.complete({
  135. error: e
  136. });
  137. });
  138. }
  139. testInfo._timeoutManager.setCurrentFixture(undefined);
  140. }
  141. async teardown(timeoutManager) {
  142. if (this._teardownWithDepsComplete) {
  143. // When we are waiting for the teardown for the second time,
  144. // most likely after the first time did timeout, annotate current fixture
  145. // for better error messages.
  146. this._setTeardownDescription(timeoutManager);
  147. await this._teardownWithDepsComplete;
  148. timeoutManager.setCurrentFixture(undefined);
  149. return;
  150. }
  151. this._teardownWithDepsComplete = this._teardownInternal(timeoutManager);
  152. await this._teardownWithDepsComplete;
  153. }
  154. _setTeardownDescription(timeoutManager) {
  155. this._runnableDescription.phase = 'teardown';
  156. timeoutManager.setCurrentFixture(this._runnableDescription);
  157. }
  158. async _teardownInternal(timeoutManager) {
  159. if (typeof this.registration.fn !== 'function') return;
  160. try {
  161. for (const fixture of this._usages) await fixture.teardown(timeoutManager);
  162. if (this._usages.size !== 0) {
  163. // TODO: replace with assert.
  164. console.error('Internal error: fixture integrity at', this._runnableDescription.title); // eslint-disable-line no-console
  165. this._usages.clear();
  166. }
  167. if (this._useFuncFinished) {
  168. (0, _util.debugTest)(`teardown ${this.registration.name}`);
  169. this._setTeardownDescription(timeoutManager);
  170. this._useFuncFinished.resolve();
  171. await this._selfTeardownComplete;
  172. timeoutManager.setCurrentFixture(undefined);
  173. }
  174. } finally {
  175. for (const dep of this._deps) dep._usages.delete(this);
  176. this.runner.instanceForId.delete(this.registration.id);
  177. }
  178. }
  179. }
  180. class FixtureRunner {
  181. constructor() {
  182. this.testScopeClean = true;
  183. this.pool = void 0;
  184. this.instanceForId = new Map();
  185. }
  186. setPool(pool) {
  187. if (!this.testScopeClean) throw new Error('Did not teardown test scope');
  188. if (this.pool && pool.digest !== this.pool.digest) {
  189. throw new Error([`Playwright detected inconsistent test.use() options.`, `Most common mistakes that lead to this issue:`, ` - Calling test.use() outside of the test file, for example in a common helper.`, ` - One test file imports from another test file.`].join('\n'));
  190. }
  191. this.pool = pool;
  192. }
  193. async teardownScope(scope, timeoutManager) {
  194. let error;
  195. // Teardown fixtures in the reverse order.
  196. const fixtures = Array.from(this.instanceForId.values()).reverse();
  197. for (const fixture of fixtures) {
  198. if (fixture.registration.scope === scope) {
  199. try {
  200. await fixture.teardown(timeoutManager);
  201. } catch (e) {
  202. if (error === undefined) error = e;
  203. }
  204. }
  205. }
  206. if (scope === 'test') this.testScopeClean = true;
  207. if (error !== undefined) throw error;
  208. }
  209. async resolveParametersForFunction(fn, testInfo, autoFixtures) {
  210. // Install automatic fixtures.
  211. const auto = [];
  212. for (const registration of this.pool.registrations.values()) {
  213. if (registration.auto === false) continue;
  214. let shouldRun = true;
  215. if (autoFixtures === 'all-hooks-only') shouldRun = registration.scope === 'worker' || registration.auto === 'all-hooks-included';else if (autoFixtures === 'worker') shouldRun = registration.scope === 'worker';
  216. if (shouldRun) auto.push(registration);
  217. }
  218. auto.sort((r1, r2) => (r1.scope === 'worker' ? 0 : 1) - (r2.scope === 'worker' ? 0 : 1));
  219. for (const registration of auto) {
  220. const fixture = await this.setupFixtureForRegistration(registration, testInfo);
  221. if (fixture.failed) return null;
  222. }
  223. // Install used fixtures.
  224. const names = getRequiredFixtureNames(fn);
  225. const params = {};
  226. for (const name of names) {
  227. const registration = this.pool.registrations.get(name);
  228. const fixture = await this.setupFixtureForRegistration(registration, testInfo);
  229. if (fixture.failed) return null;
  230. params[name] = fixture.value;
  231. }
  232. return params;
  233. }
  234. async resolveParametersAndRunFunction(fn, testInfo, autoFixtures) {
  235. const params = await this.resolveParametersForFunction(fn, testInfo, autoFixtures);
  236. if (params === null) {
  237. // Do not run the function when fixture setup has already failed.
  238. return null;
  239. }
  240. return fn(params, testInfo);
  241. }
  242. async setupFixtureForRegistration(registration, testInfo) {
  243. if (registration.scope === 'test') this.testScopeClean = false;
  244. let fixture = this.instanceForId.get(registration.id);
  245. if (fixture) return fixture;
  246. fixture = new Fixture(this, registration);
  247. this.instanceForId.set(registration.id, fixture);
  248. await fixture.setup(testInfo);
  249. return fixture;
  250. }
  251. dependsOnWorkerFixturesOnly(fn, location) {
  252. const names = getRequiredFixtureNames(fn, location);
  253. for (const name of names) {
  254. const registration = this.pool.registrations.get(name);
  255. if (registration.scope !== 'worker') return false;
  256. }
  257. return true;
  258. }
  259. }
  260. exports.FixtureRunner = FixtureRunner;
  261. function getRequiredFixtureNames(fn, location) {
  262. return (0, _fixtures.fixtureParameterNames)(fn, location !== null && location !== void 0 ? location : {
  263. file: '<unknown>',
  264. line: 1,
  265. column: 1
  266. }, e => {
  267. throw new Error(`${(0, _util.formatLocation)(e.location)}: ${e.message}`);
  268. });
  269. }