2019-11-19 05:18:28 +03:00
|
|
|
#!/usr/bin/env node
|
|
|
|
/**
|
|
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2020-12-28 18:03:09 +03:00
|
|
|
//@ts-check
|
|
|
|
|
2021-10-11 17:52:17 +03:00
|
|
|
const playwright = require('playwright-core');
|
2020-12-03 00:50:10 +03:00
|
|
|
const fs = require('fs');
|
2019-11-19 05:18:28 +03:00
|
|
|
const path = require('path');
|
2021-01-08 02:00:04 +03:00
|
|
|
const { parseApi } = require('./api_parser');
|
2021-01-02 02:17:27 +03:00
|
|
|
const missingDocs = require('./missingDocs');
|
2021-01-09 03:36:52 +03:00
|
|
|
const md = require('../markdown');
|
2020-02-14 05:26:38 +03:00
|
|
|
|
2021-01-08 02:00:04 +03:00
|
|
|
/** @typedef {import('./documentation').Type} Type */
|
2020-12-29 04:38:00 +03:00
|
|
|
/** @typedef {import('../markdown').MarkdownNode} MarkdownNode */
|
2020-12-28 18:03:09 +03:00
|
|
|
|
2019-11-19 05:18:28 +03:00
|
|
|
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
|
|
|
|
2021-01-02 02:17:27 +03:00
|
|
|
const dirtyFiles = new Set();
|
2020-12-31 05:04:51 +03:00
|
|
|
|
2020-12-03 20:21:56 +03:00
|
|
|
run().catch(e => {
|
|
|
|
console.error(e);
|
|
|
|
process.exit(1);
|
|
|
|
});;
|
2019-11-19 05:18:28 +03:00
|
|
|
|
2022-03-16 23:51:39 +03:00
|
|
|
function getAllMarkdownFiles(dirPath, filePaths = []) {
|
|
|
|
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
|
|
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
|
|
|
|
filePaths.push(path.join(dirPath, entry.name));
|
|
|
|
else if (entry.isDirectory())
|
|
|
|
getAllMarkdownFiles(path.join(dirPath, entry.name), filePaths);
|
|
|
|
}
|
|
|
|
return filePaths;
|
|
|
|
}
|
|
|
|
|
2019-11-19 05:18:28 +03:00
|
|
|
async function run() {
|
2021-01-02 02:17:27 +03:00
|
|
|
// Patch README.md
|
2021-05-18 10:29:48 +03:00
|
|
|
const versions = await getBrowserVersions();
|
2019-11-19 05:18:28 +03:00
|
|
|
{
|
2021-01-02 02:17:27 +03:00
|
|
|
const params = new Map();
|
|
|
|
const { chromium, firefox, webkit } = versions;
|
|
|
|
params.set('chromium-version', chromium);
|
|
|
|
params.set('firefox-version', firefox);
|
|
|
|
params.set('webkit-version', webkit);
|
|
|
|
params.set('chromium-version-badge', `[![Chromium version](https://img.shields.io/badge/chromium-${chromium}-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)`);
|
|
|
|
params.set('firefox-version-badge', `[![Firefox version](https://img.shields.io/badge/firefox-${firefox}-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/)`);
|
|
|
|
params.set('webkit-version-badge', `[![WebKit version](https://img.shields.io/badge/webkit-${webkit}-blue.svg?logo=safari)](https://webkit.org/)`);
|
2020-12-31 05:04:51 +03:00
|
|
|
|
2021-01-02 02:17:27 +03:00
|
|
|
let content = fs.readFileSync(path.join(PROJECT_DIR, 'README.md')).toString();
|
|
|
|
content = content.replace(/<!-- GEN:([^ ]+) -->([^<]*)<!-- GEN:stop -->/ig, (match, p1) => {
|
|
|
|
if (!params.has(p1)) {
|
|
|
|
console.log(`ERROR: Invalid generate parameter "${p1}" in "${match}"`);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
return `<!-- GEN:${p1} -->${params.get(p1)}<!-- GEN:stop -->`;
|
|
|
|
});
|
|
|
|
writeAssumeNoop(path.join(PROJECT_DIR, 'README.md'), content, dirtyFiles);
|
|
|
|
}
|
2019-11-19 05:18:28 +03:00
|
|
|
|
2022-06-28 21:37:53 +03:00
|
|
|
let playwrightVersion = require(path.join(PROJECT_DIR, 'package.json')).version;
|
|
|
|
if (playwrightVersion.endsWith('-next'))
|
|
|
|
playwrightVersion = playwrightVersion.substring(0, playwrightVersion.indexOf('-next'));
|
|
|
|
|
2022-03-16 23:51:39 +03:00
|
|
|
// Patch docker version in docs
|
|
|
|
{
|
|
|
|
const regex = new RegExp("(mcr.microsoft.com/playwright[^: ]*):?([^ ]*)");
|
|
|
|
for (const filePath of getAllMarkdownFiles(path.join(PROJECT_DIR, 'docs'))) {
|
|
|
|
let content = fs.readFileSync(filePath).toString();
|
|
|
|
content = content.replace(new RegExp('(mcr.microsoft.com/playwright[^:]*):([\\w\\d-.]+)', 'ig'), (match, imageName, imageVersion) => {
|
2022-06-28 01:31:41 +03:00
|
|
|
const [version, distroName] = imageVersion.split('-');
|
|
|
|
return `${imageName}:v${playwrightVersion}-${distroName ?? 'focal'}`;
|
2022-03-16 23:51:39 +03:00
|
|
|
});
|
|
|
|
writeAssumeNoop(filePath, content, dirtyFiles);
|
|
|
|
}
|
2022-06-28 21:37:53 +03:00
|
|
|
|
|
|
|
// Patch pom.xml
|
|
|
|
{
|
|
|
|
const introPath = path.join(PROJECT_DIR, 'docs', 'src', 'intro-java.md');
|
|
|
|
const pomVersionRe = new RegExp('^(\\s*<artifactId>playwright<\\/artifactId>\\n\\s*<version>)(.*)(<\\/version>)$', 'gm');
|
|
|
|
let content = fs.readFileSync(introPath).toString();
|
|
|
|
const majorVersion = playwrightVersion.replace(new RegExp('((\\d+\\.){2})(\\d+)'), '$10')
|
|
|
|
content = content.replace(pomVersionRe, '$1' + majorVersion + '$3');
|
|
|
|
writeAssumeNoop(introPath, content, dirtyFiles);
|
|
|
|
}
|
2022-03-16 23:51:39 +03:00
|
|
|
}
|
|
|
|
|
2021-05-18 10:29:48 +03:00
|
|
|
// Update device descriptors
|
|
|
|
{
|
2021-10-11 17:52:17 +03:00
|
|
|
const devicesDescriptorsSourceFile = path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'server', 'deviceDescriptorsSource.json')
|
2021-05-18 10:29:48 +03:00
|
|
|
const devicesDescriptors = require(devicesDescriptorsSourceFile)
|
2021-07-22 21:01:18 +03:00
|
|
|
for (const deviceName of Object.keys(devicesDescriptors)) {
|
|
|
|
switch (devicesDescriptors[deviceName].defaultBrowserType) {
|
|
|
|
case 'chromium':
|
|
|
|
devicesDescriptors[deviceName].userAgent = devicesDescriptors[deviceName].userAgent.replace(
|
|
|
|
/(.*Chrome\/)(.*?)( .*)/,
|
|
|
|
`$1${versions.chromium}$3`
|
|
|
|
).replace(
|
|
|
|
/(.*Edg\/)(.*?)$/,
|
|
|
|
`$1${versions.chromium}`
|
|
|
|
)
|
|
|
|
break;
|
|
|
|
case 'firefox':
|
|
|
|
devicesDescriptors[deviceName].userAgent = devicesDescriptors[deviceName].userAgent.replace(
|
|
|
|
/^(.*Firefox\/)(.*?)( .*?)?$/,
|
|
|
|
`$1${versions.firefox}$3`
|
2021-11-23 20:56:22 +03:00
|
|
|
).replace(/^(.*rv:)(.*)(\).*?)$/, `$1${versions.firefox}$3`)
|
2021-07-22 21:01:18 +03:00
|
|
|
break;
|
|
|
|
case 'webkit':
|
|
|
|
devicesDescriptors[deviceName].userAgent = devicesDescriptors[deviceName].userAgent.replace(
|
|
|
|
/(.*Version\/)(.*?)( .*)/,
|
|
|
|
`$1${versions.webkit}$3`
|
|
|
|
)
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2021-07-15 18:41:23 +03:00
|
|
|
}
|
2021-05-18 10:29:48 +03:00
|
|
|
writeAssumeNoop(devicesDescriptorsSourceFile, JSON.stringify(devicesDescriptors, null, 2), dirtyFiles);
|
|
|
|
}
|
|
|
|
|
2021-01-09 03:36:52 +03:00
|
|
|
// Validate links
|
|
|
|
{
|
2021-08-26 20:59:33 +03:00
|
|
|
const langs = ['js', 'java', 'python', 'csharp'];
|
2022-03-25 21:30:45 +03:00
|
|
|
const documentationRoot = path.join(PROJECT_DIR, 'docs', 'src');
|
2021-08-26 20:59:33 +03:00
|
|
|
for (const lang of langs) {
|
|
|
|
try {
|
2022-03-25 21:30:45 +03:00
|
|
|
let documentation = parseApi(path.join(documentationRoot, 'api'));
|
2021-08-26 20:59:33 +03:00
|
|
|
documentation.filterForLanguage(lang);
|
|
|
|
if (lang === 'js') {
|
2022-03-25 21:30:45 +03:00
|
|
|
const testDocumentation = parseApi(path.join(documentationRoot, 'test-api'), path.join(documentationRoot, 'api', 'params.md'));
|
2021-08-26 20:59:33 +03:00
|
|
|
testDocumentation.filterForLanguage('js');
|
2022-03-25 21:30:45 +03:00
|
|
|
const testRerpoterDocumentation = parseApi(path.join(documentationRoot, 'test-reporter-api'));
|
2021-08-26 20:59:33 +03:00
|
|
|
testRerpoterDocumentation.filterForLanguage('js');
|
|
|
|
documentation = documentation.mergeWith(testDocumentation).mergeWith(testRerpoterDocumentation);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This validates member links.
|
|
|
|
documentation.setLinkRenderer(() => undefined);
|
|
|
|
|
2022-03-25 21:30:45 +03:00
|
|
|
const relevantMarkdownFiles = new Set([...getAllMarkdownFiles(documentationRoot)
|
|
|
|
// filter out language specific files
|
|
|
|
.filter(filePath => {
|
|
|
|
const matches = filePath.match(/(-(js|python|csharp|java))+?/g);
|
|
|
|
// no language specific document
|
|
|
|
if (!matches)
|
|
|
|
return true;
|
|
|
|
// there is a language, lets filter for it
|
|
|
|
return matches.includes(`-${lang}`);
|
|
|
|
})
|
|
|
|
// Standardise naming and remove the filter in the file name
|
|
|
|
.map(filePath => filePath.replace(/(-(js|python|csharp|java))+/, ''))
|
|
|
|
// Internally (playwright.dev generator) we merge test-api and test-reporter-api into api.
|
|
|
|
.map(filePath => filePath.replace(/(\/|\\)(test-api|test-reporter-api)(\/|\\)/, `${path.sep}api${path.sep}`))]);
|
|
|
|
|
|
|
|
for (const filePath of getAllMarkdownFiles(documentationRoot)) {
|
2022-03-16 23:51:39 +03:00
|
|
|
if (langs.some(other => other !== lang && filePath.endsWith(`-${other}.md`)))
|
2021-08-26 20:59:33 +03:00
|
|
|
continue;
|
2022-03-16 23:51:39 +03:00
|
|
|
const data = fs.readFileSync(filePath, 'utf-8');
|
2022-03-25 21:30:45 +03:00
|
|
|
const rootNode = md.filterNodesForLanguage(md.parse(data), lang);
|
|
|
|
documentation.renderLinksInText(rootNode);
|
|
|
|
// Validate links
|
|
|
|
{
|
|
|
|
md.visitAll(rootNode, node => {
|
|
|
|
if (!node.text)
|
|
|
|
return;
|
|
|
|
for (const [, mdLinkName, mdLink] of node.text.matchAll(/\[([\w\s\d]+)\]\((.*?)\)/g)) {
|
|
|
|
const isExternal = mdLink.startsWith('http://') || mdLink.startsWith('https://');
|
|
|
|
if (isExternal)
|
|
|
|
continue;
|
|
|
|
// ignore links with only a hash (same file)
|
|
|
|
if (mdLink.startsWith('#'))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// The assertion classes are "virtual files" which get merged into test-assertions.md inside our docs generator
|
|
|
|
let markdownBasePath = path.dirname(filePath);
|
|
|
|
if ([
|
|
|
|
'class-screenshotassertions.md',
|
|
|
|
'class-locatorassertions.md',
|
|
|
|
'class-pageassertions.md'
|
|
|
|
].includes(path.basename(filePath))) {
|
|
|
|
markdownBasePath = documentationRoot;
|
|
|
|
}
|
|
|
|
|
|
|
|
let linkWithoutHash = path.join(markdownBasePath, mdLink.split('#')[0]);
|
|
|
|
if (path.extname(linkWithoutHash) !== '.md')
|
|
|
|
linkWithoutHash += '.md';
|
|
|
|
|
|
|
|
// We generate it inside the generator (playwright.dev)
|
|
|
|
if (path.basename(linkWithoutHash) === 'test-assertions.md')
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!relevantMarkdownFiles.has(linkWithoutHash))
|
|
|
|
throw new Error(`${path.relative(PROJECT_DIR, filePath)} references to '${linkWithoutHash}' as '${mdLinkName}' which does not exist.`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2021-08-26 20:59:33 +03:00
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
e.message = `While processing "${lang}"\n` + e.message;
|
|
|
|
throw e;
|
|
|
|
}
|
2021-01-09 03:36:52 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-02 02:17:27 +03:00
|
|
|
// Check for missing docs
|
|
|
|
{
|
2021-08-26 20:59:33 +03:00
|
|
|
const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
|
|
|
|
apiDocumentation.filterForLanguage('js');
|
2021-10-11 17:52:17 +03:00
|
|
|
const srcClient = path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'client');
|
2021-01-02 02:17:27 +03:00
|
|
|
const sources = fs.readdirSync(srcClient).map(n => path.join(srcClient, n));
|
2021-07-22 21:01:18 +03:00
|
|
|
const errors = missingDocs(apiDocumentation, sources, path.join(srcClient, 'api.ts'));
|
2021-01-02 02:17:27 +03:00
|
|
|
if (errors.length) {
|
|
|
|
console.log('============================');
|
|
|
|
console.log('ERROR: missing documentation:');
|
|
|
|
errors.forEach(e => console.log(e));
|
|
|
|
console.log('============================')
|
|
|
|
process.exit(1);
|
2019-11-19 05:18:28 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-02 02:17:27 +03:00
|
|
|
if (dirtyFiles.size) {
|
|
|
|
console.log('============================')
|
2021-10-08 23:43:23 +03:00
|
|
|
console.log('ERROR: generated files have changed, this is only error if happens in CI:');
|
2021-01-02 02:17:27 +03:00
|
|
|
[...dirtyFiles].forEach(f => console.log(f));
|
|
|
|
console.log('============================')
|
|
|
|
process.exit(1);
|
2019-11-19 05:18:28 +03:00
|
|
|
}
|
2021-01-02 02:17:27 +03:00
|
|
|
process.exit(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} name
|
|
|
|
* @param {string} content
|
|
|
|
* @param {Set<string>} dirtyFiles
|
|
|
|
*/
|
|
|
|
function writeAssumeNoop(name, content, dirtyFiles) {
|
2021-01-03 19:47:29 +03:00
|
|
|
fs.mkdirSync(path.dirname(name), { recursive: true });
|
|
|
|
const oldContent = fs.existsSync(name) ? fs.readFileSync(name).toString() : '';
|
2021-01-02 02:17:27 +03:00
|
|
|
if (oldContent !== content) {
|
|
|
|
fs.writeFileSync(name, content);
|
|
|
|
dirtyFiles.add(name);
|
2019-11-19 05:18:28 +03:00
|
|
|
}
|
|
|
|
}
|
2020-02-14 05:26:38 +03:00
|
|
|
|
2020-03-13 00:03:01 +03:00
|
|
|
async function getBrowserVersions() {
|
2020-12-22 05:09:55 +03:00
|
|
|
const names = ['chromium', 'firefox', 'webkit'];
|
|
|
|
const browsers = await Promise.all(names.map(name => playwright[name].launch()));
|
|
|
|
const result = {};
|
|
|
|
for (let i = 0; i < names.length; i++) {
|
|
|
|
result[names[i]] = browsers[i].version();
|
2020-03-13 00:03:01 +03:00
|
|
|
}
|
2020-12-22 05:09:55 +03:00
|
|
|
await Promise.all(browsers.map(browser => browser.close()));
|
|
|
|
return result;
|
2020-03-13 00:03:01 +03:00
|
|
|
}
|