pin playwright to 1.8 to avoid regressions, and fix flaky tests

Playwright had some in-range breaking changes (regressions): https://github.com/microsoft/playwright/issues/10819 and https://github.com/microsoft/playwright/issues/11570

Playwright tests need to `await onceRendered()` after each route navigation to wait for render to finish before testing the state of the rendered DOM. Tests were flaky because they were looking for DOM before render finished (sometimes).

Additionally, this fixes one test that was failing only locally, but not in CI, due to a RegExp check against page.url() (not sure why it would differ on CI vs local, but now the URL is explicit).
This commit is contained in:
Joe Pea 2022-01-23 00:00:15 -08:00
parent 9658ed4d3a
commit 38f2d243b8
9 changed files with 183 additions and 42146 deletions

4
.gitignore vendored
View File

@ -8,3 +8,7 @@ themes/
# exceptions
!.gitkeep
# libraries don't need lock files, only apps do.
package-lock.json
yarn.lock

View File

@ -118,7 +118,7 @@
'/zh-cn/': '搜索',
'/': 'Search',
},
pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn']
pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn'],
},
vueComponents: {
'button-counter': {
@ -208,6 +208,31 @@
);
});
},
/**
* Docsify tests rely on this plugin.
*
* This plugin increments a `data-render-count="N"` attribute on the
* `<body>` element after each markdown page render. F.e.
* `data-render-count="1"`, `data-render-count="2"`, etc.
*
* The very first render from calling docsifyInit() will result in a
* value of 1. Each following render (f.e. from clicking a link) will
* increment once rendering of the new page is finished.
*
* We do this so that we can easily wait for a render to finish before
* testing the state of the render result, or else we'll have flaky
* tests (a "race condition").
*/
function RenderCountPlugin(hook) {
hook.init(() => {
document.body.dataset.renderCount = 0;
});
hook.doneEach(() => {
document.body.dataset.renderCount++;
});
},
],
};
</script>

View File

@ -82,7 +82,7 @@
'/zh-cn/': '搜索',
'/': 'Search',
},
pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn']
pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn'],
},
plugins: [
DocsifyCarbon.create('CEBI6KQE', 'docsifyjsorg'),
@ -110,6 +110,31 @@
);
});
},
/**
* Docsify tests rely on this plugin.
*
* This plugin increments a `data-render-count="N"` attribute on the
* `<body>` element after each markdown page render. F.e.
* `data-render-count="1"`, `data-render-count="2"`, etc.
*
* The very first render from calling docsifyInit() will result in a
* value of 1. Each following render (f.e. from clicking a link) will
* increment once rendering of the new page is finished.
*
* We do this so that we can easily wait for a render to finish before
* testing the state of the render result, or else we'll have flaky
* tests (a "race condition").
*/
function RenderCountPlugin(hook) {
hook.init(() => {
document.body.dataset.renderCount = 0;
});
hook.doneEach(() => {
document.body.dataset.renderCount++;
});
},
],
};
</script>

42125
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -109,7 +109,7 @@
"live-server": "^1.2.1",
"mkdirp": "^0.5.1",
"npm-run-all": "^4.1.5",
"playwright": "^1.8.0",
"playwright": "1.8.0",
"prettier": "^1.19.1",
"rimraf": "^3.0.0",
"rollup": "^2.63.0",

View File

@ -26,7 +26,7 @@ describe(`Example Tests`, function() {
await expect(page).toHaveText('body', 'Test');
await expect(page).toHaveSelector('p');
await expect(page).toEqualText('p', testText);
await expect(page).not.toHaveSelector('table', { timeout: 1 });
await expect(page).not.toHaveSelector('table', { timeout: 15000 });
// Test using standard jest + playwrite methods
// https://playwright.dev/#path=docs%2Fapi.md&q=pagetextcontentselector-options
@ -78,13 +78,16 @@ describe(`Example Tests`, function() {
);
}
const functionResult = await page.evaluate(`
// This one requires the code to be wrapped in an IIFE or else there's a
// regression between playwright v1.8 and v1.17 that causes an error,
// https://github.com/microsoft/playwright/issues/10819
const functionResult = await page.evaluate(`(() => {
${add.toString()}
const result = add(1, 2, 3);
Promise.resolve(result);
`);
return Promise.resolve(result);
})()`);
expect(functionResult).toBe(6);
});

View File

@ -15,14 +15,15 @@ describe(`Index file hosting`, function() {
'#main',
'A magical documentation site generator'
);
expect(page.url()).toMatch(/index\.html#\/$/);
expect(page.url()).toMatch(sharedOptions.testURL);
});
test('should use index file links in sidebar from index file hosting', async () => {
await docsifyInit(sharedOptions);
const onceRendered = await docsifyInit(sharedOptions);
await page.click('a[href="#/quickstart"]');
await onceRendered();
await expect(page).toHaveText('#main', 'Quick start');
expect(page.url()).toMatch(/index\.html#\/quickstart$/);
expect(page.url()).toMatch(sharedOptions.testURL + 'quickstart');
});
});

View File

@ -39,32 +39,39 @@ describe('Sidebar Tests', function() {
},
};
await docsifyInit(docsifyInitConfig);
const onceRendered = await docsifyInit(docsifyInitConfig);
await page.click('a[href="#/test%20space"]');
await expect(page).toEqualText(
await onceRendered();
await expect(page).toMatchText(
'.sidebar-nav li[class=active]',
'Test Space'
);
expect(page.url()).toMatch(/\/test%20space$/);
await page.click('a[href="#/test_foo"]');
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test _');
await onceRendered();
await expect(page).toMatchText('.sidebar-nav li[class=active]', 'Test _');
expect(page.url()).toMatch(/\/test_foo$/);
await page.click('a[href="#/test-foo"]');
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test -');
await onceRendered();
await expect(page).toMatchText('.sidebar-nav li[class=active]', 'Test -');
expect(page.url()).toMatch(/\/test-foo$/);
await page.click('a[href="#/test.foo"]');
await onceRendered();
await expect(page).toMatchText('.sidebar-nav li[class=active]', 'Test .');
expect(page.url()).toMatch(/\/test.foo$/);
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test .');
await page.click('a[href="#/test>foo"]');
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test >');
await onceRendered();
await expect(page).toMatchText('.sidebar-nav li[class=active]', 'Test >');
expect(page.url()).toMatch(/\/test%3Efoo$/);
await page.click('a[href="#/test"]');
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test');
await onceRendered();
await expect(page).toMatchText('.sidebar-nav li[class=active]', 'Test');
expect(page.url()).toMatch(/\/test$/);
});
});

View File

@ -2,6 +2,8 @@
import mock, { proxy } from 'xhr-mock';
import { waitForSelector } from './wait-for';
// TODO use browser.newPage() instead of re-using the same page every time?
const axios = require('axios');
const prettier = require('prettier');
const stripIndent = require('common-tags/lib/stripIndent');
@ -11,6 +13,12 @@ const docsifyURL = '/lib/docsify.js'; // Playwright
const isJSDOM = 'window' in global;
const isPlaywright = 'page' in global;
jest.setTimeout(35_000);
if (isPlaywright) {
page.setDefaultNavigationTimeout(30_000);
page.setDefaultTimeout(30_000);
}
/**
* Jest / Playwright helper for creating custom docsify test sites
*
@ -33,13 +41,37 @@ const isPlaywright = 'page' in global;
* @param {Boolean|Object|String} [options._logHTML] Logs HTML to console after initialization. Accepts CSS selector.
* @param {Boolean} [options._logHTML.format=true] Formats HTML output
* @param {String} [options._logHTML.selector='html'] CSS selector(s) to match and log HTML for
* @returns {Promise}
* @returns {Promise<() => Promise<void>>}
*/
async function docsifyInit(options = {}) {
const defaults = {
config: {
basePath: TEST_HOST,
el: '#app',
plugins: [
/**
* This plugin increments a `data-render-count="N"` attribute on the
* `<body>` element after each markdown page render. F.e.
* `data-render-count="1"`, `data-render-count="2"`, etc.
*
* The very first render from calling docsifyInit() will result in a
* value of 1. Each following render (f.e. from clicking a link) will
* increment once rendering of the new page is finished.
*
* We do this so that we can easily wait for a render to finish before
* testing the state of the render result, or else we'll have flaky
* tests (a "race condition").
*/
function RenderCountPlugin(hook) {
hook.init(() => {
document.body.dataset.renderCount = 0;
});
hook.doneEach(() => {
document.body.dataset.renderCount++;
});
},
],
},
html: `
<!DOCTYPE html>
@ -87,7 +119,15 @@ async function docsifyInit(options = {}) {
// Config as function
if (typeof options.config === 'function') {
return function(vm) {
const config = { ...sharedConfig, ...options.config(vm) };
const outsideConfig = options.config(vm);
const config = {
...sharedConfig,
...outsideConfig,
plugins: [
...sharedConfig.plugins,
...(outsideConfig?.plugins ?? []),
],
};
updateBasePath(config);
@ -96,7 +136,12 @@ async function docsifyInit(options = {}) {
}
// Config as object
else {
const config = { ...sharedConfig, ...options.config };
const outsideConfig = options.config;
const config = {
...sharedConfig,
...outsideConfig,
plugins: [...sharedConfig.plugins, ...(outsideConfig?.plugins ?? [])],
};
updateBasePath(config);
@ -360,7 +405,59 @@ async function docsifyInit(options = {}) {
}
}
return Promise.resolve();
// We must use a separate renderCount variable outside of the
// RenderCountPlugin, because the plugin runs in scope of the Playwright
// browser context, not in scope of this file. We can't read
// `document.body.renderCount` directly here.
let renderCount = 0;
/**
* Call this function to wait for the render to finish any time you perform an
* action that changes Docsify's route and causes a new markdown page to
* render.
*
* This is needed only for e2e tests that run in playwright (so far, at least).
*
* We use page.waitForSelector here to wait for each render or else there is a
* race condition where we will try to observe the state of the page between
* clicking a link and when the new content is actually updated.
*/
async function onceRendered() {
// This function is only for playwright tests.
if (isJSDOM) {
return;
}
// Initial case: For the first render, there is a bug
// (https://github.com/docsifyjs/docsify/issues/1732) that sometimes causes
// doneEach to render twice.
if (renderCount === 0) {
renderCount = await Promise.race([
page.waitForSelector(`body[data-render-count="1"]`).then(() => 1),
page.waitForSelector(`body[data-render-count="2"]`).then(() => 2),
]);
}
// Successive cases after first render: doneEach fires once like
// normal (so far).
else {
renderCount++;
try {
await page.waitForSelector(`body[data-render-count="${renderCount}"]`);
} catch (e) {
// eslint-disable-next-line no-console
console.error(
'Render counting failed because doneEach fired an unexpected number of times (more than once). If this happens, please note this in issue https://github.com/docsifyjs/docsify/issues/1732'
);
}
}
}
// Wait for the initial render.
await onceRendered();
// Now all tests need to call this any time they change Docsify navagtion
// (f.e. clicking a link) to wait for render.
return onceRendered;
}
module.exports = docsifyInit;