From 6b2ca8dd2e3fd90c2264679cf27c2b65f86ead5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 7 Aug 2025 16:33:51 +0200 Subject: [PATCH] support rebuild testing for Meteor-rspack integration --- tools/modern-tests/assertions.js | 12 ++- tools/modern-tests/helpers.js | 161 +++++++++++++++++++++++++++++++ tools/modern-tests/react.test.js | 43 ++++++++- 3 files changed, 211 insertions(+), 5 deletions(-) diff --git a/tools/modern-tests/assertions.js b/tools/modern-tests/assertions.js index b4be00b2b3..f14ff6d731 100644 --- a/tools/modern-tests/assertions.js +++ b/tools/modern-tests/assertions.js @@ -8,19 +8,25 @@ import path from 'path'; /** * Helper function to assert that a Meteor React app is running correctly * @param {number} port - Port where the app is running + * @param {Object} options - Options for the assertion + * @param {string} options.title - Expected content in the title (default: "react") + * @param {string} options.h1 - Expected content in the h1 element (default: "Welcome to Meteor!") * @returns {Promise} */ -export async function assertMeteorReactApp(port) { +export async function assertMeteorReactApp(port, options = {}) { + // Extract options with default values + const { title: inTitle = "react", h1: inH1 = "Welcome to Meteor!" } = options; + // Navigate to the app await page.goto(`http://localhost:${port}`); // Check the title const title = await page.title(); - expect(title).toMatch(/react/); + expect(title).toMatch(new RegExp(inTitle)); // Check for static content const h1Text = await page.$eval('h1', el => el.textContent); - expect(h1Text).toMatch(/Welcome to Meteor!/); + expect(h1Text).toMatch(new RegExp(inH1)); } /** diff --git a/tools/modern-tests/helpers.js b/tools/modern-tests/helpers.js index 41d42d05e5..9dbea3168b 100644 --- a/tools/modern-tests/helpers.js +++ b/tools/modern-tests/helpers.js @@ -326,3 +326,164 @@ export async function waitForMeteorOutput(outputLines, pattern, options = {}) { checkForPattern(); }); } + +/** + * Helper function to replace specific text within a file in a temporary directory + * This is useful for triggering file change detection in tests + * @param {string} tempDir - Path to the temporary directory + * @param {string} filePath - Path to the file relative to tempDir + * @param {Object} options - Additional options + * @param {string} options.searchText - Text to search for in the file + * @param {string} options.replaceText - Text to replace the searchText with + * @param {boolean} options.createIfNotExists - Create the file if it doesn't exist (default: true) + * @returns {Promise} - A promise that resolves when the file has been updated + */ +export async function replaceFileContent(tempDir, filePath, options = {}) { + const { searchText, replaceText, createIfNotExists = true } = options; + const fullPath = path.join(tempDir, filePath); + + console.log(`Replacing text in file: ${fullPath}`); + + try { + // Check if file exists + const fileExists = await fs.pathExists(fullPath); + + if (!fileExists) { + if (!createIfNotExists) { + throw new Error(`File does not exist: ${fullPath}`); + } + // Create directory structure if it doesn't exist + await fs.ensureDir(path.dirname(fullPath)); + // Create an empty file + await fs.writeFile(fullPath, '', 'utf8'); + } else { + // Read the existing content + const content = await fs.readFile(fullPath, 'utf8'); + + // Replace the specified text + const newContent = content.replace(searchText, replaceText); + + // Write the modified content back to the file + await fs.writeFile(fullPath, newContent, 'utf8'); + } + + console.log(`Successfully replaced text in file: ${fullPath}`); + } catch (err) { + console.error(`Error replacing text in file ${fullPath}:`, err); + throw err; + } +} + +/** + * Helper function to append content to a file in a temporary directory + * This is useful for adding code to files during tests + * @param {string} tempDir - Path to the temporary directory + * @param {string} filePath - Path to the file relative to tempDir + * @param {string} content - Content to append to the file + * @param {Object} options - Additional options + * @param {boolean} options.createIfNotExists - Create the file if it doesn't exist (default: true) + * @param {string} options.separator - Separator to add before the appended content (default: '\n') + * @returns {Promise} - A promise that resolves when the file has been updated + */ +export async function appendFileContent(tempDir, filePath, options = {}) { + const { createIfNotExists = true, separator = '\n', content = '' } = options; + const fullPath = path.join(tempDir, filePath); + + console.log(`Appending content to file: ${fullPath}`); + + try { + // Check if file exists + const fileExists = await fs.pathExists(fullPath); + + if (!fileExists) { + if (!createIfNotExists) { + throw new Error(`File does not exist: ${fullPath}`); + } + // Create directory structure if it doesn't exist + await fs.ensureDir(path.dirname(fullPath)); + // Create the file with the content + await fs.writeFile(fullPath, content, 'utf8'); + } else { + // Read the existing content + const existingContent = await fs.readFile(fullPath, 'utf8'); + + // Append the new content with a separator + const newContent = existingContent + separator + content; + + // Write the modified content back to the file + await fs.writeFile(fullPath, newContent, 'utf8'); + } + + console.log(`Successfully appended content to file: ${fullPath}`); + } catch (err) { + console.error(`Error appending content to file ${fullPath}:`, err); + throw err; + } +} + +/** + * Helper function to wait for a specific console message from a Playwright page + * @param {Object} page - The Playwright page object + * @param {string|RegExp} pattern - String or RegExp pattern to wait for in console messages + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds (default: 30000) + * @param {number} options.checkInterval - Interval between checks in milliseconds (default: 100) + * @returns {Promise} - A promise that resolves with the matched console message + */ +export async function waitForPlaywrightConsole(page, pattern, options = {}) { + const timeout = options.timeout || 30000; // Default 30 seconds timeout + const checkInterval = options.checkInterval || 100; // Check every 100ms by default + + console.log(`Waiting for console message matching: ${pattern}`); + + // Array to collect console messages + const consoleMessages = []; + + // Create a named listener function so we can remove it later + const consoleListener = (msg) => { + const text = msg.text(); + consoleMessages.push(text); + console.log(`Browser console: ${text}`); + }; + + // Set up console message listener + page.on('console', consoleListener); + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + // Function to check for the pattern in the console messages + const checkForPattern = () => { + // Check if we've exceeded the timeout + if (Date.now() - startTime > timeout) { + // Remove the listener before rejecting + page.removeListener('console', consoleListener); + reject(new Error(`Timeout waiting for console message matching: ${pattern}`)); + return; + } + + // Check each message for the pattern + for (const message of consoleMessages) { + if (typeof pattern === 'string' && message.includes(pattern)) { + console.log(`Found console message matching string: ${pattern}`); + // Remove the listener before resolving + page.removeListener('console', consoleListener); + resolve(message); + return; + } else if (pattern instanceof RegExp && pattern.test(message)) { + console.log(`Found console message matching regex: ${pattern}`); + // Remove the listener before resolving + page.removeListener('console', consoleListener); + resolve(message); + return; + } + } + + // If we didn't find a match, check again after the interval + setTimeout(checkForPattern, checkInterval); + }; + + // Start checking + checkForPattern(); + }); +} diff --git a/tools/modern-tests/react.test.js b/tools/modern-tests/react.test.js index 46c6c5c5cf..b7a9f2a879 100644 --- a/tools/modern-tests/react.test.js +++ b/tools/modern-tests/react.test.js @@ -5,12 +5,15 @@ import { cleanupTempDir, killMeteorProcess, createMeteorApp, - runMeteorCommand, wait + runMeteorCommand, + wait, + appendFileContent, + waitForMeteorOutput, + waitForPlaywrightConsole, } from "./helpers"; import { assertMeteorReactApp, assertRspackScriptTag, assertFileExist } from './assertions'; import fs from 'fs-extra'; import path from 'path'; -import waitOn from "wait-on"; describe('React App Bundling', () => { describe('Meteor Creator', () => { @@ -164,6 +167,24 @@ describe('React App Bundling', () => { // Assert that the app is using Rspack await assertRspackScriptTag(PORT, true); + // Update the client code + await appendFileContent(tempDir, 'client/main.jsx', { + content: 'if (Meteor.isDevelopment) console.log("Hello from dev client");', + }); + await waitForPlaywrightConsole(page, 'Hello from dev client'); + + // Update the server code + await appendFileContent(tempDir, 'server/main.js', { + content: 'if (Meteor.isDevelopment) console.log("Hello from dev server");', + }); + await waitForMeteorOutput( + result.outputLines, + 'Hello from dev server' + ); + + // Wait for a margin + await wait(500); + // Kill the meteor process await killMeteorProcess(meteorProcess); @@ -199,6 +220,24 @@ describe('React App Bundling', () => { // Assert that the app is using Rspack await assertRspackScriptTag(PORT, false); + // Update the client code + await appendFileContent(tempDir, 'client/main.jsx', { + content: 'if (Meteor.isProduction) console.log("Hello from prod client");', + }); + await waitForPlaywrightConsole(page, 'Hello from prod client'); + + // Update the server code + await appendFileContent(tempDir, 'server/main.js', { + content: 'if (Meteor.isProduction) console.log("Hello from prod server");', + }); + await waitForMeteorOutput( + result.outputLines, + 'Hello from prod server' + ); + + // Wait for a margin + await wait(500); + // Kill the meteor process await killMeteorProcess(meteorProcess);