Skip to content

Commit dba304a

Browse files
committed
chore(test): migrate bundler test from Karma to Jest; add sub-config and ci jobs
1 parent c7e2830 commit dba304a

File tree

8 files changed

+162
-254
lines changed

8 files changed

+162
-254
lines changed

.github/workflows/ci.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
7+
jobs:
8+
test-core:
9+
name: Core Jest
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
- name: Setup Node
15+
uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
cache: 'npm'
19+
- name: Install
20+
run: npm ci
21+
- name: Run Jest (core)
22+
run: npm run test.jest
23+
24+
test-bundler:
25+
name: Bundler Jest
26+
runs-on: ubuntu-latest
27+
needs: test-core
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
- name: Setup Node
32+
uses: actions/setup-node@v4
33+
with:
34+
node-version: 20
35+
cache: 'npm'
36+
- name: Install
37+
run: npm ci
38+
- name: Run Bundler tests
39+
run: npm run test.bundler
40+
41+

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@
129129
"test.docs-build": "cd test && npm run build.docs-json && npm run build.docs-readme",
130130
"test.watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
131131
"test.watch-all": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watchAll --coverage",
132+
"test.bundler": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js -c test/bundler/jest.config.js --passWithNoTests",
133+
"ci.test": "npm run test.jest && npm run test.bundler",
132134
"tsc.prod": "tsc",
133135
"ts": "tsc --noEmit --project scripts/tsconfig.json && tsx"
134136
},

test/bundler/jest-dom-utils.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { pathToFileURL } from 'url';
4+
5+
export type DomTestUtilities = {
6+
setupDom: (htmlPathFromRepoRoot: string) => Promise<HTMLElement>;
7+
tearDownDom: () => void;
8+
};
9+
10+
export function setupDomTests(document: Document): DomTestUtilities {
11+
let testBed = document.getElementById('test-app');
12+
if (!testBed) {
13+
testBed = document.createElement('div');
14+
testBed.id = 'test-app';
15+
document.body.appendChild(testBed);
16+
}
17+
18+
async function setupDom(htmlPathFromRepoRoot: string): Promise<HTMLElement> {
19+
if (!testBed) {
20+
throw new Error('The Stencil/Jest test bed could not be found.');
21+
}
22+
const testElement = document.createElement('div');
23+
testElement.className = 'test-spec';
24+
testBed.appendChild(testElement);
25+
26+
const absPath = path.resolve(process.cwd(), htmlPathFromRepoRoot.replace(/^\//, ''));
27+
const html = fs.readFileSync(absPath, 'utf-8');
28+
testElement.innerHTML = html;
29+
30+
// execute module scripts referenced by the built index.html so components register
31+
const baseDir = path.dirname(absPath);
32+
const scripts = Array.from(testElement.querySelectorAll('script')) as HTMLScriptElement[];
33+
for (const s of scripts) {
34+
const type = (s.getAttribute('type') || '').toLowerCase();
35+
const src = s.getAttribute('src');
36+
if (type === 'module' && src && src.endsWith('.js')) {
37+
const rel = src.startsWith('/') ? src.slice(1) : src;
38+
const jsAbs = path.resolve(baseDir, rel);
39+
const fileUrl = pathToFileURL(jsAbs);
40+
// dynamic import to execute the built bundle
41+
// eslint-disable-next-line no-await-in-loop
42+
await import(fileUrl.href);
43+
}
44+
}
45+
46+
// wait for app readiness similar to Karma helper
47+
await new Promise<void>((resolve) => {
48+
const onAppLoad = () => {
49+
window.removeEventListener('appload', onAppLoad);
50+
resolve();
51+
};
52+
window.addEventListener('appload', onAppLoad);
53+
// if app already loaded synchronously, resolve on next tick
54+
setTimeout(() => resolve(), 0);
55+
});
56+
57+
await allReady();
58+
return testElement;
59+
}
60+
61+
function tearDownDom(): void {
62+
if (testBed) {
63+
testBed.innerHTML = '';
64+
}
65+
}
66+
67+
async function allReady(): Promise<void> {
68+
const promises: Promise<any>[] = [];
69+
const waitForDidLoad = (elm: Element): void => {
70+
if (elm != null && elm.nodeType === 1) {
71+
for (let i = 0; i < elm.children.length; i++) {
72+
const childElm = elm.children[i] as any;
73+
if (childElm.tagName && childElm.tagName.includes('-') && typeof childElm.componentOnReady === 'function') {
74+
promises.push(childElm.componentOnReady());
75+
}
76+
waitForDidLoad(childElm);
77+
}
78+
}
79+
};
80+
waitForDidLoad(window.document.documentElement);
81+
await Promise.all(promises).catch(() => undefined);
82+
}
83+
84+
return { setupDom, tearDownDom };
85+
}
86+
87+

test/bundler/jest.config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const path = require('path');
2+
const base = require('../../jest.config.js');
3+
4+
const rootDir = path.resolve(__dirname, '../..');
5+
const modulePathIgnorePatterns = (base.modulePathIgnorePatterns || []).filter(
6+
(p) => !/\<rootDir\>\/test\//.test(p)
7+
);
8+
9+
module.exports = {
10+
rootDir,
11+
testEnvironment: base.testEnvironment || 'jsdom',
12+
setupFilesAfterEnv: base.setupFilesAfterEnv || ['<rootDir>/testing/jest-setuptestframework.js'],
13+
transform: base.transform,
14+
moduleNameMapper: base.moduleNameMapper,
15+
moduleFileExtensions: base.moduleFileExtensions,
16+
testPathIgnorePatterns: base.testPathIgnorePatterns || [],
17+
modulePathIgnorePatterns,
18+
testRegex: '/test/bundler/.*\\.spec\\.ts$',
19+
};
20+
21+

test/bundler/karma-stencil-utils.ts

Lines changed: 3 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ const path = require('path');
22

33
// we must use a relative path here instead of tsconfig#paths
44
// see https://github.com/monounity/karma-typescript/issues/315
5-
import * as d from '../../internal';
5+
// Deprecated: replaced by jest-dom-utils.ts
6+
export {};
67

78
/**
89
* Utilities for creating a test bed to execute HTML rendering tests against
@@ -26,193 +27,4 @@ type DomTestUtilities = {
2627
* @param document a `Document` compliant entity where tests may be rendered
2728
* @returns utilities to set up the DOM and tear it down within the test bed
2829
*/
29-
export function setupDomTests(document: Document): DomTestUtilities {
30-
/**
31-
* All HTML will be rendered as a child of the test bed - get it from the current document (and create it, if it
32-
* doesn't exist) so that it is available for all future tests.
33-
*/
34-
let testBed = document.getElementById('test-app');
35-
if (!testBed) {
36-
testBed = document.createElement('div');
37-
testBed.id = 'test-app';
38-
document.body.appendChild(testBed);
39-
}
40-
41-
/**
42-
* @see {@link DomTestUtilities#setupDom}
43-
*/
44-
function setupDom(url: string): Promise<HTMLElement> {
45-
const testElement = document.createElement('div');
46-
testElement.className = 'test-spec';
47-
48-
if (!testBed) {
49-
console.error('The Stencil/Karma test bed could not be found.');
50-
process.exit(1);
51-
}
52-
53-
testBed.appendChild(testElement);
54-
55-
return renderTest(url, testElement);
56-
}
57-
58-
/**
59-
* Render HTML for executing tests against.
60-
* @param url the location on disk containing the HTML to load
61-
* @param testElement a parent HTML element to place test code in
62-
* @returns the fully rendered HTML to test against
63-
*/
64-
function renderTest(url: string, testElement: HTMLElement): Promise<HTMLElement> {
65-
// 'base' is the directory that karma will serve all assets from
66-
url = path.join('base', url);
67-
68-
return new Promise<HTMLElement>((resolve, reject) => {
69-
/**
70-
* Callback to be invoked following the retrieval of the file containing the HTML to load
71-
* @param this the `XMLHttpRequest` instance that requested the HTML
72-
*/
73-
const indexHtmlLoaded = function (this: XMLHttpRequest): void {
74-
if (this.status !== 200) {
75-
reject(`404: ${url}`);
76-
return;
77-
}
78-
79-
testElement.innerHTML = this.responseText;
80-
81-
/**
82-
* Re-generate script tags that are embedded in the loaded HTML file.
83-
*
84-
* Doing so allows JS files to be loaded (via script tags), when the HTML is served, without having to configure
85-
* Karma to load the JS explicitly. This is done by adding the host/port combination to existing `src`
86-
* attributes.
87-
*
88-
* Before:
89-
* ```html
90-
* <script type="module" src="/index.6127a5ed.js"></script>
91-
* ```
92-
*
93-
* After:
94-
* ```html
95-
* <script src="http://localhost:9876/index.547a265b.js" type="module"></script>
96-
* ```
97-
*/
98-
const parseAndRebuildScriptTags = () => {
99-
const tempScripts: NodeListOf<HTMLScriptElement> = testElement.querySelectorAll('script');
100-
for (let i = 0; i < tempScripts.length; i++) {
101-
const script: HTMLScriptElement = document.createElement('script');
102-
if (tempScripts[i].src) {
103-
script.src = tempScripts[i].src;
104-
}
105-
if (tempScripts[i].hasAttribute('nomodule')) {
106-
script.setAttribute('nomodule', '');
107-
}
108-
if (tempScripts[i].hasAttribute('type')) {
109-
const typeAttribute = tempScripts[i].getAttribute('type');
110-
if (typeof typeAttribute === 'string') {
111-
// older DOM implementations would return an empty string to designate `null`
112-
// here, we interpret the empty string to be a valid value
113-
script.setAttribute('type', typeAttribute);
114-
}
115-
}
116-
script.innerHTML = tempScripts[i].innerHTML;
117-
118-
if (tempScripts[i].parentNode) {
119-
// the scripts were found by querying a common parent node, which _should_ still exist
120-
tempScripts[i].parentNode!.insertBefore(script, tempScripts[i]);
121-
tempScripts[i].parentNode!.removeChild(tempScripts[i]);
122-
} else {
123-
// if for some reason the parent node no longer exists, something's manipulated it while we were parsing
124-
// the script tags. this can lead to undesirable & hard to debug behavior, fail.
125-
reject('the parent node for script tags no longer exists. exiting.');
126-
}
127-
}
128-
};
129-
130-
parseAndRebuildScriptTags();
131-
132-
/**
133-
* Create a listener for Stencil's "appload" event to signal to the test framework the application and its
134-
* children have finished loading
135-
*/
136-
const onAppLoad = () => {
137-
window.removeEventListener('appload', onAppLoad);
138-
allReady().then(() => {
139-
resolve(testElement);
140-
});
141-
};
142-
window.addEventListener('appload', onAppLoad);
143-
};
144-
145-
/**
146-
* Ensure that all `onComponentReady` functions on Stencil elements in the DOM have been called before rendering
147-
* @returns an array of promises, one for each `onComponentReady` found on a Stencil component
148-
*/
149-
const allReady = (): Promise<d.HTMLStencilElement[] | void> => {
150-
const promises: Promise<d.HTMLStencilElement>[] = [];
151-
152-
/**
153-
* Function that recursively traverses the DOM, looking for Stencil components. Any `componentOnReady`
154-
* functions found on Stencil components are pushed to a buffer to be run after traversing the entire DOM.
155-
* @param elm the current element being inspected
156-
*/
157-
const waitForDidLoad = (elm: Element): void => {
158-
if (elm != null && elm.nodeType === 1) {
159-
// the element exists and is an `ELEMENT_NODE`
160-
for (let i = 0; i < elm.children.length; i++) {
161-
const childElm = elm.children[i];
162-
if (childElm.tagName.includes('-') && isHtmlStencilElement(childElm)) {
163-
promises.push(childElm.componentOnReady());
164-
}
165-
waitForDidLoad(childElm);
166-
}
167-
}
168-
};
169-
170-
// recursively walk the DOM to find all `onComponentReady` functions
171-
waitForDidLoad(window.document.documentElement);
172-
173-
return Promise.all(promises).catch((e) => console.error(e));
174-
};
175-
176-
try {
177-
const testHtmlRequest = new XMLHttpRequest();
178-
testHtmlRequest.addEventListener('load', indexHtmlLoaded);
179-
testHtmlRequest.addEventListener('error', (err) => {
180-
console.error('error testHtmlRequest.addEventListener', err);
181-
reject(err);
182-
});
183-
testHtmlRequest.open('GET', url);
184-
testHtmlRequest.send();
185-
} catch (e: unknown) {
186-
console.error('catch error', e);
187-
reject(e);
188-
}
189-
});
190-
}
191-
192-
/**
193-
* @see {@link DomTestUtilities#tearDownDom}
194-
*/
195-
function tearDownDom(): void {
196-
if (testBed) {
197-
testBed.innerHTML = '';
198-
}
199-
}
200-
201-
return { setupDom, tearDownDom };
202-
}
203-
204-
/**
205-
* Type guard to verify some entity is an instance of Stencil HTML Element
206-
* @param elm the entity to test
207-
* @returns `true` if the entity is a Stencil HTML Element, `false` otherwise
208-
*/
209-
function isHtmlStencilElement(elm: unknown): elm is d.HTMLStencilElement {
210-
// `hasOwnProperty` does not act as a type guard/narrow `elm` in any way, so we use an assertion to verify that
211-
// `onComponentReady` is a function
212-
return (
213-
elm != null &&
214-
typeof elm === 'object' &&
215-
elm.hasOwnProperty('onComponentReady') &&
216-
typeof (elm as any).onComponentReady === 'function'
217-
);
218-
}
30+
//

0 commit comments

Comments
 (0)