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+ }
0 commit comments