Skip to content

Commit 21c0f29

Browse files
committed
feat: Configures testing with Playwright.
1 parent a163ae7 commit 21c0f29

23 files changed

+747
-251
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Ignore all node_modules including in subdirectories for samples/
22
**/node_modules/
3+
**/test-results/
34
samples/.env
45

56
# (temporary) Ignore dist files generated by build

e2e/example.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
import { test, expect } from '@playwright/test';
218
/**
3-
// NOTE: Trying to graft the old js-samples tests in here to see what happens.
19+
// NOTE: KEEP THIS CODE. It contains things you need to add.
420
import { waitForGoogleMapsToLoad, failOnPageError } from "./utils";
521
import fs from "fs";
622

e2e/samples.spec.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from '@playwright/test';
18+
import fs from 'fs';
19+
import path from 'path';
20+
import childProcess from 'child_process';
21+
import { assert } from 'console';
22+
23+
const samplesDir = path.join(__dirname, '..', 'samples');
24+
25+
const sampleFolders = fs.readdirSync(samplesDir).filter((file) => {
26+
return fs.statSync(path.join(samplesDir, file)).isDirectory();
27+
});
28+
29+
// Iterate through samples and run the same test for each one.
30+
sampleFolders.forEach((sampleFolder) => {
31+
test(`test ${sampleFolder}`, async ({ page }) => {
32+
33+
// START Build the sample
34+
const buildProcess = childProcess.spawn('npm', ['run', 'build'], {
35+
cwd: path.join(samplesDir, sampleFolder),
36+
stdio: 'inherit',
37+
});
38+
39+
await new Promise((resolve, reject) => {
40+
buildProcess.on('close', (code) => {
41+
if (code === 0) {
42+
resolve(true);
43+
} else {
44+
reject(`Build process exited with code ${code}`);
45+
}
46+
});
47+
});
48+
// END Build the sample
49+
50+
// START run the preview
51+
// Get an available port
52+
const port = 8080;
53+
54+
const url = `http://localhost:${port}/`;
55+
56+
const viteProcess = childProcess.spawn('vite', ['preview', `--port=${port}`], {
57+
cwd: path.join(samplesDir, sampleFolder),
58+
stdio: 'inherit',
59+
});
60+
61+
await new Promise((resolve) => setTimeout(resolve, 500)); // Set a timeout to let the web server start.
62+
// END run the preview
63+
64+
/**
65+
* Run all of the tests. Each method call either runs a test or inserts a timeout for loading.
66+
* `expect`s are assertions that test for conditions.
67+
* Run `npx playwright test --ui` to launch Playwright in UI mode to iteratively debug this file.
68+
*/
69+
try {
70+
await page.goto(url);
71+
72+
// Wait for the page DOM to load; this does NOT include the Google Maps APIs.
73+
await page.waitForLoadState('domcontentloaded');
74+
75+
// Wait for Google Maps to load.
76+
await page.waitForFunction(() => window.google && window.google.maps);
77+
78+
// Insert a delay in ms to let the map load.
79+
await new Promise((resolve) => setTimeout(resolve, 1000));
80+
81+
82+
// Yo dawg, I heard you like tests, so I made you a test for testing your tests.
83+
//await expect(page).toHaveTitle('Simple Map'); // Passes on the simple map page, fails on the other as expected.
84+
85+
// Assertions. These must be met or the test will fail.
86+
87+
// The sample must load the Google Maps API.
88+
// IMPORTANT: Ignore the squigglies. `google` is not expected to resolve in the IDE, but at runtime it is global.
89+
const hasGoogleMaps = await page.evaluate(() => {
90+
return typeof window.google !== 'undefined' && typeof window.google.maps !== 'undefined';
91+
});
92+
// The sample must load the Google Maps API.
93+
assertWithLabel('Google Maps API is loaded.', () => {
94+
expect(hasGoogleMaps).toBeTruthy();
95+
});
96+
97+
// Listen for all console events and handle errors.
98+
// Create an array to hold all error strings.
99+
const consoleErrors: string[] = [];
100+
// If an error occurs, add the message string to the array.
101+
page.on('console', msg => {
102+
if (msg.type() === 'error') {
103+
consoleErrors.push(msg.text());
104+
}
105+
});
106+
107+
// If a page error occurs, add the message string to the array.
108+
page.on('pageerror', (exception) => {
109+
console.error('Page Error:', exception.message);
110+
consoleErrors.push(exception.message);
111+
});
112+
113+
// TODO: This assert is not catching the missing DIV console error. WHY?
114+
// Right now it's kind of a lie, but not blocking on this.
115+
// There must be no console errors.
116+
assertWithLabel('App loads without error.', () => {
117+
expect(consoleErrors).toHaveLength(0);
118+
});
119+
120+
/**
121+
* TODO: Implement conditional logic for samples with no map, such as Places API.
122+
* Maybe different tests that are conditionally assigned? I think you can do that.
123+
* What is the best way to check for the existence of visible things which will have many different names and text representations?
124+
*/
125+
126+
// Verify that the map element is visible.
127+
// The toBeVisible() assertion fails headlessly if using assertWithLabel().
128+
const mapElement = await page.locator('#map');
129+
if (await page.locator('#map').isVisible()) {
130+
console.log(`✅ Assertion passed: Map is visible.`);
131+
} else {
132+
console.error(`❌ Assertion failed: Map is not visible.`);
133+
throw new Error('Assertion failed: Map is not visible.');
134+
}
135+
136+
137+
} finally {
138+
viteProcess.kill();
139+
}
140+
});
141+
});
142+
143+
// TODO: Verify that this doesn't do weird stuff with async (as in the above noted isVisible doesn't work headlessly when )
144+
// Helper function to log assertions with custom labels.
145+
async function assertWithLabel(label: string, assertion: () => Promise<void> | void) {
146+
try {
147+
await assertion();
148+
console.log(`✅ Assertion passed: ${label}`);
149+
} catch (error) {
150+
console.error(`❌ Assertion failed: ${label}`);
151+
throw error; // Re-throw the error to fail the test
152+
}
153+
}

e2e/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
import { Page } from "@playwright/test";
218

319
// from https://github.com/lit/lit.dev/blob/5d79d1e0989e68f8b5905e5271229ffe4c55265c/packages/lit-dev-tests/src/playwright/util.ts
420

21+
// Probably don't need this.
522
export async function waitForGoogleMapsToLoad(page: Page) {
623
await page.waitForFunction(() => window.google && window.google.maps);
24+
return typeof window.google !== 'undefined' && typeof window.google.maps !== 'undefined';
725
await page.waitForTimeout(100);
826
}
927

28+
// Definitely don't need this.
1029
export const failOnPageError = (page: Page) => {
1130
page.on("pageerror", (e) => {
1231
console.error(e.message);
1332
process.emit("uncaughtException", e);
1433
});
1534
};
1635

36+
// Will probably need these, and others such as Places (not nondeterministic but lacks #map in most cases).
1737
export const NONDETERMINISTIC_SAMPLES = [
1838
"move-camera-ease", // camera always moving
1939
"map-puzzle", // random puzzle placement

0 commit comments

Comments
 (0)