@atomicsmash/init-testing 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, readdir, appendFile } from "node:fs/promises";
3
+ import { dirname as pathDirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import yargs from "yargs";
6
+ import { hideBin } from "yargs/helpers";
7
+ import { PackageManager } from "./utils.js";
8
+ // eslint-disable-next-line @typescript-eslint/naming-convention
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ // eslint-disable-next-line @typescript-eslint/naming-convention
11
+ const __dirname = pathDirname(__filename);
12
+ const packageManager = new PackageManager(await import(`${process.cwd()}/package.json`).catch(() => {
13
+ throw new Error("Unable to find package.json in your cwd. Make sure you're running this command in the right folder.");
14
+ }), await import(`${process.cwd()}/package-lock.json`).catch(() => {
15
+ throw new Error("Unable to find package-lock.json in your cwd. Make sure you're running this command in the right folder.");
16
+ }));
17
+ const argv = await yargs(hideBin(process.argv))
18
+ .options({
19
+ "base-url": {
20
+ demandOption: true,
21
+ describe: "Base URL to use in actions like `await page.goto('/')`",
22
+ type: "string",
23
+ },
24
+ "no-example": {
25
+ boolean: true,
26
+ default: false,
27
+ describe: "If present, don't copy over the playwright example tests.",
28
+ },
29
+ "no-universal": {
30
+ boolean: true,
31
+ default: false,
32
+ describe: "If present, don't copy over the Atomic Smash universal tests (NOT RECOMMENDED).",
33
+ },
34
+ })
35
+ .showHelpOnFail(false, "Specify --help for available options")
36
+ .parse();
37
+ const fileCopies = [];
38
+ const copyFiles = await readdir(`${__dirname}/../toCopy`, {
39
+ withFileTypes: true,
40
+ recursive: true,
41
+ });
42
+ for (const dirent of copyFiles) {
43
+ console.log({ name: dirent.name, path: dirent.path });
44
+ if (!dirent.isFile()) {
45
+ continue;
46
+ }
47
+ if (argv.noExample && dirent.name === "example.spec.ts") {
48
+ continue;
49
+ }
50
+ if (argv.noUniversal && dirent.name === "universal.spec.ts") {
51
+ continue;
52
+ }
53
+ fileCopies.push(readFile(dirent.path, "utf8")
54
+ .then(async (data) => {
55
+ for (const [search, replace] of [
56
+ ["%%BASE_URL%%", argv.baseUrl],
57
+ ]) {
58
+ data = data.replace(search, replace);
59
+ }
60
+ await writeFile(dirent.path, data, "utf8");
61
+ return `${dirent.name} copied successfully.`;
62
+ })
63
+ .catch((error) => `${dirent.name} failed to copy. Error: ${error}`));
64
+ }
65
+ await Promise.all([
66
+ Promise.allSettled([
67
+ packageManager.ensurePackageIsInstalled("@atomicsmash/test-utils", {
68
+ packageConstraint: "^1.0.0",
69
+ type: "dev",
70
+ }),
71
+ packageManager.ensurePackageIsInstalled("@playwright/test", {
72
+ packageConstraint: "^1.0.0",
73
+ type: "dev",
74
+ }),
75
+ packageManager.ensurePackageIsInstalled("close-with-grace", {
76
+ packageConstraint: "^1.0.0",
77
+ type: "dev",
78
+ }),
79
+ packageManager.ensurePackageIsInstalled("msw", {
80
+ packageConstraint: "^2.0.0",
81
+ type: "dev",
82
+ }),
83
+ packageManager.ensurePackageIsInstalled("cross-env", {
84
+ packageConstraint: "^7.0.0",
85
+ type: "dev",
86
+ }),
87
+ async () => {
88
+ const [major, minor] = process.versions.node.split(".");
89
+ return packageManager.ensurePackageIsInstalled("@types/node", {
90
+ packageConstraint: `${major}.${minor}.x`,
91
+ type: "dev",
92
+ });
93
+ },
94
+ ]).then((results) => {
95
+ if (results.some((result) => {
96
+ return result.status === "fulfilled" && result.value === true;
97
+ })) {
98
+ packageManager.runCommands();
99
+ }
100
+ }),
101
+ Promise.allSettled(fileCopies).then((results) => {
102
+ results.forEach((result) => {
103
+ console.log(result.status === "fulfilled" ? result.value : result.reason);
104
+ });
105
+ }),
106
+ appendFile(`${process.cwd()}/.gitignore`, `
107
+
108
+ # Testing tools
109
+ /test-results/
110
+ /playwright-report/
111
+ /blob-report/
112
+ /playwright/.cache/
113
+ `),
114
+ ]);
@@ -0,0 +1,51 @@
1
+ export declare class PackageManager {
2
+ private packageJson;
3
+ private packageLockJson;
4
+ commands: {
5
+ /**
6
+ * Basic commands, each entry must be its own command.
7
+ */
8
+ basic: string[];
9
+ /**
10
+ * Packages to install as normal dependencies with their version constraints.
11
+ */
12
+ install: string[];
13
+ /**
14
+ * Packages to install as dev dependencies with their version constraints.
15
+ */
16
+ devInstall: string[];
17
+ };
18
+ constructor(packageJson: unknown, packageLockJson: unknown);
19
+ /**
20
+ * Run all the queued commands and then clear the commands object.
21
+ */
22
+ runCommands(): void;
23
+ /**
24
+ * Get information about the package if it's installed.
25
+ * @param packageName The package name of the package to get info for.
26
+ * @returns Various information about the package if installed, null if not installed.
27
+ */
28
+ getDependencyInfo(packageName: string): {
29
+ type: "normal";
30
+ packageConstraint: string;
31
+ currentVersion: string;
32
+ } | {
33
+ type: "dev";
34
+ packageConstraint: string;
35
+ currentVersion: string;
36
+ } | null;
37
+ /**
38
+ * This method will check if a dependency is installed, and if it isn't, it will add a command to the commands property.
39
+ * @param packageName The package name of the package to check.
40
+ * @param options (Optional) Options for installing the package.
41
+ * @param options.packageConstraint (Optional) What version of the package to install. Defaults to `"*"`.
42
+ * @param options.type (Optional) What type of dependency to install it as. Accepts `"normal"` or `"dev"`. Defaults to `"normal"`.
43
+ * @returns Whether a command was added to the commands prompt.
44
+ * @throws If the package cannot be installed.
45
+ */
46
+ ensurePackageIsInstalled(packageName: string, options?: {
47
+ packageConstraint?: string;
48
+ type?: "normal" | "dev";
49
+ }): Promise<boolean>;
50
+ }
51
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAqBA,qBAAa,cAAc;IAC1B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,eAAe,CAAkB;IACzC,QAAQ;QACP;;WAEG;;QAEH;;WAEG;;QAEH;;WAEG;;MAEF;gBAEU,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO;IAK1D;;OAEG;IACH,WAAW;IAaX;;;;OAIG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM;;;;;;;;;IA2BrC;;;;;;;;OAQG;IACG,wBAAwB,CAC7B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;QACT,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,IAAI,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;KACxB;CA8FF"}
package/dist/utils.js ADDED
@@ -0,0 +1,130 @@
1
+ import { exec } from "node:child_process";
2
+ import { gtr as greaterThanRange, ltr as lessThanRange, validRange, } from "semver";
3
+ export class PackageManager {
4
+ packageJson;
5
+ packageLockJson;
6
+ commands = {
7
+ /**
8
+ * Basic commands, each entry must be its own command.
9
+ */
10
+ basic: [],
11
+ /**
12
+ * Packages to install as normal dependencies with their version constraints.
13
+ */
14
+ install: [],
15
+ /**
16
+ * Packages to install as dev dependencies with their version constraints.
17
+ */
18
+ devInstall: [],
19
+ };
20
+ constructor(packageJson, packageLockJson) {
21
+ this.packageJson = packageJson;
22
+ this.packageLockJson = packageLockJson;
23
+ }
24
+ /**
25
+ * Run all the queued commands and then clear the commands object.
26
+ */
27
+ runCommands() {
28
+ for (const basicCommand of this.commands.basic) {
29
+ exec(basicCommand);
30
+ }
31
+ exec(`npm install --save ${this.commands.install.join(" ")}`);
32
+ exec(`npm install --save-dev ${this.commands.devInstall.join(" ")}`);
33
+ this.commands = {
34
+ basic: [],
35
+ install: [],
36
+ devInstall: [],
37
+ };
38
+ }
39
+ /**
40
+ * Get information about the package if it's installed.
41
+ * @param packageName The package name of the package to get info for.
42
+ * @returns Various information about the package if installed, null if not installed.
43
+ */
44
+ getDependencyInfo(packageName) {
45
+ if (this.packageJson.dependencies &&
46
+ packageName in this.packageJson.dependencies) {
47
+ return {
48
+ type: "normal",
49
+ packageConstraint: this.packageJson.dependencies[packageName] ?? "",
50
+ currentVersion: this.packageLockJson.packages[`node_modules/${packageName}`]
51
+ ?.version ?? "",
52
+ };
53
+ }
54
+ if (this.packageJson.devDependencies &&
55
+ packageName in this.packageJson.devDependencies) {
56
+ return {
57
+ type: "dev",
58
+ packageConstraint: this.packageJson.devDependencies[packageName] ?? "",
59
+ currentVersion: this.packageLockJson.packages[`node_modules/${packageName}`]
60
+ ?.version ?? "",
61
+ };
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * This method will check if a dependency is installed, and if it isn't, it will add a command to the commands property.
67
+ * @param packageName The package name of the package to check.
68
+ * @param options (Optional) Options for installing the package.
69
+ * @param options.packageConstraint (Optional) What version of the package to install. Defaults to `"*"`.
70
+ * @param options.type (Optional) What type of dependency to install it as. Accepts `"normal"` or `"dev"`. Defaults to `"normal"`.
71
+ * @returns Whether a command was added to the commands prompt.
72
+ * @throws If the package cannot be installed.
73
+ */
74
+ async ensurePackageIsInstalled(packageName, options) {
75
+ return new Promise((resolve, reject) => {
76
+ const { packageConstraint, type } = {
77
+ type: "normal",
78
+ packageConstraint: "*",
79
+ ...options,
80
+ };
81
+ const packageConstraintRange = validRange(packageConstraint);
82
+ if (packageConstraintRange === null) {
83
+ throw new Error("The packageConstraint value you entered is not a valid semver range.");
84
+ }
85
+ const dependencyInfo = this.getDependencyInfo(packageName);
86
+ if (type === "dev") {
87
+ if (dependencyInfo === null) {
88
+ console.log(`${packageName} is missing, we will automatically install it for you.`);
89
+ this.commands.devInstall.push(`${packageName}@${packageConstraintRange}`);
90
+ return resolve(true);
91
+ }
92
+ if (greaterThanRange(dependencyInfo.currentVersion, packageConstraintRange)) {
93
+ console.log(`The package ${packageName} is already installed and is newer than the requested version. You may encounter issues with your testing setup.`);
94
+ return reject("NEWER_PACKAGE_FOUND");
95
+ }
96
+ if (lessThanRange(dependencyInfo.currentVersion, packageConstraintRange)) {
97
+ console.log(`The package ${packageName} is already installed and is older than the requested version. You should manually update the package to match the range: ${packageConstraintRange}.`);
98
+ return reject("OLDER_PACKAGE_FOUND");
99
+ }
100
+ console.log(`An acceptable version of ${packageName} is already installed, skipping...`);
101
+ return resolve(false);
102
+ }
103
+ if (dependencyInfo === null) {
104
+ console.log(`${packageName} is missing, we will automatically install it for you.`);
105
+ this.commands.install.push(`${packageName}@${packageConstraintRange}`);
106
+ return resolve(true);
107
+ }
108
+ const isFoundAsDevDependency = dependencyInfo.type === "dev";
109
+ if (greaterThanRange(dependencyInfo.currentVersion, packageConstraintRange)) {
110
+ console.log(`The package ${packageName} is already installed and is newer than the requested version. You may encounter issues with your testing setup.${isFoundAsDevDependency
111
+ ? ` You should also move this dependency to be a normal dependency.`
112
+ : ""}`);
113
+ return reject("NEWER_PACKAGE_FOUND");
114
+ }
115
+ if (lessThanRange(dependencyInfo.currentVersion, packageConstraintRange)) {
116
+ console.log(`The package ${packageName} is already installed and is older than the requested version. You should manually update the package to match the range: ${packageConstraintRange}.${isFoundAsDevDependency
117
+ ? ` You should also move this dependency to be a normal dependency.`
118
+ : ""}`);
119
+ return reject("OLDER_PACKAGE_FOUND");
120
+ }
121
+ if (isFoundAsDevDependency) {
122
+ console.log(`An acceptable version of ${packageName} is already installed, updating it to a normal dependency...`);
123
+ this.commands.install.push(`${packageName}@${packageConstraintRange}`);
124
+ return resolve(true);
125
+ }
126
+ console.log(`An acceptable version of ${packageName} is already installed, skipping...`);
127
+ return resolve(false);
128
+ });
129
+ }
130
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@atomicsmash/init-testing",
3
+ "type": "module",
4
+ "version": "1.0.0-beta.0",
5
+ "description": "Allow us to easily add automated testing setup to any npm setup.",
6
+ "keywords": [
7
+ "atomic",
8
+ "smash",
9
+ "automated testing",
10
+ "test",
11
+ "testing"
12
+ ],
13
+ "homepage": "https://www.atomicsmash.co.uk/",
14
+ "license": "GPL-3.0-or-later",
15
+ "author": {
16
+ "name": "Atomic Smash",
17
+ "email": "npm@atomicsmash.co.uk",
18
+ "url": "https://www.atomicsmash.co.uk/"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/AtomicSmash/packages",
23
+ "directory": "packages/init-testing"
24
+ },
25
+ "files": [
26
+ "./dist/**/*",
27
+ "./toCopy/**/*"
28
+ ],
29
+ "bin": "./dist/cli.js",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "dev": "tsc --build --watch tsconfig.build.json",
35
+ "build": "npm-run-all clean compile",
36
+ "clean": "del-cli ./dist/ *.tsbuildinfo",
37
+ "compile": "tsc --build tsconfig.build.json",
38
+ "test": "TZ=\"Europe/Istanbul\" vitest run",
39
+ "test:ui": "TZ=\"Europe/Istanbul\" vitest --ui",
40
+ "test:coverage": "TZ=\"Europe/Istanbul\" vitest run --coverage",
41
+ "lint": "run-p lint:*",
42
+ "lint:eslint": "eslint --cache --cache-location ./node_modules/.cache/eslint . --ext .js,.ts,.jsx,.tsx",
43
+ "lint:types": "tsc"
44
+ },
45
+ "dependencies": {
46
+ "semver": "^7.6.0",
47
+ "yargs": "^17.7.2"
48
+ }
49
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "@playwright/test";
2
+ import { playwrightConfig } from "@atomicsmash/test-utils";
3
+
4
+ export default defineConfig(playwrightConfig, {
5
+ use: {
6
+ baseURL: `%%BASE_URL%%`,
7
+ },
8
+ });
@@ -0,0 +1,18 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test('has title', async ({ page }) => {
4
+ await page.goto('https://playwright.dev/');
5
+
6
+ // Expect a title "to contain" a substring.
7
+ await expect(page).toHaveTitle(/Playwright/);
8
+ });
9
+
10
+ test('get started link', async ({ page }) => {
11
+ await page.goto('https://playwright.dev/');
12
+
13
+ // Click the get started link.
14
+ await page.getByRole('link', { name: 'Get started' }).click();
15
+
16
+ // Expects page to have a heading with the name of Installation.
17
+ await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
18
+ });
@@ -0,0 +1,23 @@
1
+ import {
2
+ doLighthouseTest,
3
+ checkPrivilegedPages,
4
+ checkForLoremIpsum,
5
+ } from "@atomicsmash/test-utils";
6
+ import { test, lighthouseTest, pagesToTest } from "../playwright-utils";
7
+
8
+ test(
9
+ "privileged pages of the site to ensure non-logged in users can't access them",
10
+ checkPrivilegedPages(pagesToTest),
11
+ );
12
+ test("No lingering lorem ipsum on the site", checkForLoremIpsum(pagesToTest));
13
+
14
+ for (const pageToTest of pagesToTest) {
15
+ lighthouseTest(
16
+ `${pageToTest.name} @desktop @lighthouse`,
17
+ doLighthouseTest(pageToTest, "desktop", true),
18
+ );
19
+ lighthouseTest(
20
+ `${pageToTest.name} @mobile @lighthouse`,
21
+ doLighthouseTest(pageToTest, "mobile", true),
22
+ );
23
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * This file adds static test data to be available in all your tests.
3
+ * Currently, it only contains an array of valid phone numbers, but
4
+ * this could include any data you want.
5
+ */
6
+ const phoneNumbers = [
7
+ '01234567890',
8
+ '+441234567890',
9
+ '00441234567890',
10
+ '0123 45 6789',
11
+ '0123456789',
12
+ '0123456789',
13
+ '(01234) 567 890',
14
+ ]
15
+
16
+ export const testData = {
17
+ phoneNumbers,
18
+ }
@@ -0,0 +1,16 @@
1
+ import type { RequestHandler } from 'msw'
2
+ import closeWithGrace from 'close-with-grace'
3
+ import { http } from 'msw'
4
+ import { setupServer } from 'msw/node'
5
+
6
+ const handlers: RequestHandler[] = []
7
+
8
+ const server = setupServer(...handlers)
9
+
10
+ server.listen({ onUnhandledRequest: 'warn' })
11
+ // eslint-disable-next-line no-console
12
+ console.info('🔶 Mock server installed')
13
+
14
+ closeWithGrace({ delay: 1000 }, () => {
15
+ server.close()
16
+ })
@@ -0,0 +1,42 @@
1
+ import type {
2
+ AuthenticatePageFunction,
3
+ PageList,
4
+ } from '@atomicsmash/test-utils'
5
+ import type { Page } from '@playwright/test'
6
+ import { getLighthouseTest } from '@atomicsmash/test-utils'
7
+ import { test as base } from '@playwright/test'
8
+ import { testData } from './fixtures/testData.js'
9
+
10
+ /**
11
+ * Fixtures
12
+ */
13
+ const logInUser: AuthenticatePageFunction = async function ({ page }, use) {
14
+ // Do any actions here to log a user in so you can access logged in areas of the site.
15
+ // This is useful for testing my account pages, etc.
16
+ await use(page)
17
+ }
18
+
19
+ type CustomFixtures = {
20
+ testData: typeof testData
21
+ authenticatedPage: Page
22
+ }
23
+
24
+ export const test = base.extend<CustomFixtures>({
25
+ testData: async (
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
+ { page },
28
+ use
29
+ ) => {
30
+ // Use the fixture value in the test.
31
+ await use(testData)
32
+ },
33
+ authenticatedPage: [logInUser, { scope: 'test' }],
34
+ })
35
+
36
+ export const lighthouseTest = getLighthouseTest(logInUser)
37
+ export const { expect } = test // re-export expect to cleanup imports in tests
38
+ export { checkAccessibility } from '@atomicsmash/test-utils'
39
+
40
+ export const pagesToTest: PageList = [
41
+ // hover over PageList to see the format required for each page
42
+ ]