Skip to content

Commit

Permalink
build: configure Node.js esm hooks
Browse files Browse the repository at this point in the history
This allows configuring Web Test Runner with Typescript,
importing TypeScript/Sass files during tests for SSR
and verifying the entry points from dist packages
that import from `@sbb-esta/*`.
  • Loading branch information
kyubisation committed May 24, 2024
1 parent f30a57b commit 9b15570
Show file tree
Hide file tree
Showing 21 changed files with 558 additions and 312 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.11.0
v22.2.0
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@
"lint:tsc:visual-regression-app": "tsc --noEmit --project src/visual-regression-app/tsconfig.json",
"start": "storybook dev -p 6006",
"start:visual-regression-app": "vite --config src/visual-regression-app/vite.config.ts",
"test": "wtr --coverage",
"test": "yarn -s wtr --coverage",
"test:snapshot": "yarn test:csr --ci --update-snapshots",
"test:csr": "wtr --group default",
"test:ssr:hydrated": "wtr --group e2e-ssr-hydrated",
"test:ssr:non-hydrated": "wtr --group e2e-ssr-non-hydrated",
"test:csr": "yarn -s wtr --group default",
"test:ssr:hydrated": "yarn -s wtr --group e2e-ssr-hydrated",
"test:ssr:non-hydrated": "yarn -s wtr --group e2e-ssr-non-hydrated",
"test:visual-regression": "tsx tools/visual-regression-testing/exec.ts --group=visual-regression --all-browsers",
"wtr": "node --no-deprecation --import ./tools/node-esm-hook/register-hooks.js node_modules/@web/test-runner/dist/bin.js --config=web-test-runner.config.ts",
"prepare": "husky"
},
"dependencies": {
Expand Down Expand Up @@ -99,6 +100,7 @@
"chromatic": "11.4.0",
"custom-elements-manifest": "2.1.0",
"date-fns": "3.6.0",
"esbuild": "0.21.3",
"esbuild-sass-plugin": "3.3.0",
"eslint": "9.3.0",
"eslint-config-prettier": "9.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/components/core/testing/private/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export async function visualRegressionFixture<T extends HTMLElement>(
): Promise<T> {
const fix = await fixture<T>(
html`<div
id="visual-regression-fixture-wrapper"
style=${`padding: ${wrapperStyles?.padding ?? '2rem'};background-color: ${wrapperStyles?.backgroundColor ?? 'var(--sbb-color-white)'};${wrapperStyles?.focusOutlineDark ? ' --sbb-focus-outline-color: var(--sbb-focus-outline-color-dark);' : ''}`}
tabindex="1"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { aTimeout } from '@open-wc/testing';
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
import { visualDiff } from '@web/test-runner-visual-regression';

import { waitForCondition } from '../wait-for-condition.js';

export function imageName(test: Mocha.Runnable): string {
return test!.fullTitle().replaceAll(', ', '-').replaceAll(' ', '_');
}
Expand All @@ -30,12 +28,9 @@ export function testVisualDiff(snapshotElement: () => HTMLElement): void {

export function testVisualDiffFocus(snapshotElement: () => HTMLElement): void {
it('focus', async function () {
await waitForCondition(() => {
snapshotElement().focus();
return document.activeElement === snapshotElement();
});
snapshotElement().focus();
await sendKeys({ press: 'Tab' });
await aTimeout(60);
//await aTimeout(60);
await visualDiff(snapshotElement(), imageName(this.test!));
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/react/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
generateReactWrappers,
isProdBuild,
packageJsonTemplate,
verifyEntryPoints,
} from '../../tools/vite/index.js';
import rootConfig from '../../vite.config.js';

Expand All @@ -15,7 +16,9 @@ export default defineConfig((config) =>
plugins: [
generateReactWrappers(),
...(config.command === 'build' ? [dts()] : []),
...(isProdBuild(config) ? [packageJsonTemplate({ exportsExtensions: ['', '.js'] })] : []),
...(isProdBuild(config)
? [packageJsonTemplate({ exportsExtensions: ['', '.js'] }), verifyEntryPoints()]
: []),
],
build: {
lib: {
Expand Down
18 changes: 18 additions & 0 deletions tools/node-esm-hook/lyne-alias-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @ts-check

const dist = new URL('../../dist/', import.meta.url).href;

/**
* @param {string} specifier - The specifier of the resource to resolve.
* @param {object} context - The context in which the resolve function is called.
* @param {function} nextResolve - The function to call if nothing is done.
* @returns {Promise} - A Promise that resolves with an object containing the format, shortCircuit flag, and url of the resource,
* or rejects with an error.
*/
export async function resolve(specifier, context, nextResolve) {
if (specifier.startsWith('@sbb-esta/lyne-') && context.parentURL?.startsWith(dist)) {
const aliasUrl = new URL(specifier.replace(/^@sbb-esta\/lyne-/, './'), dist).href;
return { format: 'module', shortCircuit: true, url: aliasUrl };
}
return nextResolve(specifier, context);
}
4 changes: 4 additions & 0 deletions tools/node-esm-hook/register-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { register } from 'node:module';

register('./sass-hook.js', import.meta.url);
register('./typescript-hook.js', import.meta.url);
31 changes: 31 additions & 0 deletions tools/node-esm-hook/sass-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-check

import { fileURLToPath } from 'node:url';

import { initCompiler } from 'sass';

const root = new URL('../../', import.meta.url).href;
const sassCompiler = initCompiler();
const compileSass = (/** @type {string} */ fileUrl) =>
sassCompiler.compile(fileURLToPath(fileUrl), {
loadPaths: ['.', './node_modules/'],
}).css;

/**
* @param {string} url - The URL of the resource to load as a string.
* @param {object} context - The context in which the load function is called.
* @param {function} nextLoad - The function to call if this loader does nothing.
* @returns {Promise} - A Promise that resolves with an object containing the format, shortCircuit flag, and source of the resource,
* or rejects with an error.
*/
export function load(url, context, nextLoad) {
if (url.startsWith(root) && url.includes('.scss?')) {
const source = `import { css } from 'lit';\nexport default css\`${compileSass(url)}\`;`;
return Promise.resolve({
format: 'module',
shortCircuit: true,
source,
});
}
return nextLoad(url, context);
}
57 changes: 57 additions & 0 deletions tools/node-esm-hook/typescript-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @ts-check

import { existsSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

import { transform } from 'esbuild';

const root = new URL('../../', import.meta.url).href;
const tsconfigRaw = readFileSync(new URL('./tsconfig.json', root), 'utf8');

/**
* @param {string} specifier - The specifier of the resource to resolve.
* @param {object} context - The context in which the resolve function is called.
* @param {function} nextResolve - The function to call if nothing is done.
* @returns {Promise} - A Promise that resolves with an object containing the format, shortCircuit flag, and url of the resource,
* or rejects with an error.
*/
export async function resolve(specifier, context, nextResolve) {
if (
(specifier.startsWith('.') || specifier.startsWith(root)) &&
!specifier.includes('/node_modules/') &&
context.parentURL?.startsWith(root)
) {
const originalUrl = new URL(specifier, context.parentURL).href;
const url = originalUrl.replace(/.js$/, '.ts');
if (!existsSync(fileURLToPath(originalUrl)) && existsSync(fileURLToPath(url))) {
return { format: 'module', shortCircuit: true, url };
}
}
return nextResolve(specifier, context);
}

/**
* @param {string} url - The URL of the resource to load as a string.
* @param {object} context - The context in which the load function is called.
* @param {function} nextLoad - The function to call if this loader does nothing.
* @returns {Promise} - A Promise that resolves with an object containing the format, shortCircuit flag, and source of the resource,
* or rejects with an error.
*/
export async function load(url, context, nextLoad) {
if (url.startsWith(root) && !url.includes('/node_modules/') && url.endsWith('.ts')) {
const sourcefile = fileURLToPath(url);
const content = readFileSync(sourcefile, 'utf8');
const result = await transform(content, {
loader: 'ts',
tsconfigRaw,
/*
TODO: sourcemap support does not seem to work, even with --enable-source-maps. Figure out why.
sourcefile: url,
sourcemap: 'inline',
sourcesContent: false,
*/
});
return { format: 'module', shortCircuit: true, source: result.code };
}
return nextLoad(url, context);
}
5 changes: 4 additions & 1 deletion tools/vite/verify-entry-points.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { join, relative } from 'path';
import { register } from 'node:module';
import { join, relative } from 'node:path';

import type { LibraryOptions, PluginOption, ResolvedConfig } from 'vite';

register('../node-esm-hook/lyne-alias-hook.js', import.meta.url);

export function verifyEntryPoints(): PluginOption {
let viteConfig: ResolvedConfig;
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* eslint-disable import-x/export */
export * from './minimal-reporter.js';
export * from './patched-summary-reporter.js';
export * from './ssr-plugin.js';
export * from './visual-regression-plugin-config.js';
export * from './vite-plugin.js';
102 changes: 0 additions & 102 deletions tools/web-test-runner/lit-ssr-worker.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import { dotReporter } from '@web/test-runner';
import {
dotReporter,
type Reporter,
type TestResult,
type TestSession,
type TestSuiteResult,
} from '@web/test-runner';

/** @type {import('@web/test-runner-core').Reporter} */
export function minimalReporter() {
export function minimalReporter(): Reporter {
const base = dotReporter();
const log = (result) =>
const log = (result: TestResult): boolean =>
process.stdout.write(result.passed ? '.' : result.skipped ? '~' : '\x1b[31mx\x1b[0m');
function logResults(results) {
function logResults(results: TestSuiteResult | undefined): void {
results?.tests?.forEach(log);
results?.suites?.forEach(logResults);
}
const collectorBase = { suites: 0, tests: 0, passed: 0, failed: 0, skipped: 0 };
/** @param {import('@web/test-runner-core').TestSession[]} sessions */
function aggregateSessions(sessions) {
const browserResults = new Map();
function aggregateSessions(sessions: TestSession[]): [string, typeof collectorBase][] {
const browserResults = new Map<string, typeof collectorBase>();
for (const session of sessions) {
if (!browserResults.has(session.browser.name)) {
browserResults.set(session.browser.name, { ...collectorBase });
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
aggregateSuites(session.testResults, browserResults.get(session.browser.name));
}
return Array.from(browserResults).sort((a, b) => a[0].localeCompare(b[0]));
}
/** @param {import('@web/test-runner-core').TestSuiteResult} results */
function aggregateSuites(results, collector = { ...collectorBase }) {
function aggregateSuites(
results: TestSuiteResult | undefined,
collector = { ...collectorBase },
): typeof collectorBase {
collector.suites += 1;
results?.tests?.forEach((result) => {
collector.tests += 1;
Expand All @@ -37,7 +44,7 @@ export function minimalReporter() {
results?.suites?.forEach((s) => aggregateSuites(s, collector));
return collector;
}
const p = (v) => v.toString().padStart(6, ' ');
const p = (v: number): string => v.toString().padStart(6, ' ');

return {
...base,
Expand All @@ -61,7 +68,7 @@ export function minimalReporter() {
console.groupEnd();
}
console.groupEnd();
base.onTestRunFinished(args);
base.onTestRunFinished!(args);
},
};
}
Loading

0 comments on commit 9b15570

Please sign in to comment.