support rebuild testing for Meteor-rspack integration

This commit is contained in:
Nacho Codoñer
2025-08-07 16:33:51 +02:00
parent 170f4bf872
commit 6b2ca8dd2e
3 changed files with 211 additions and 5 deletions

View File

@@ -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<void>}
*/
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));
}
/**

View File

@@ -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<void>} - 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<void>} - 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<string>} - 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();
});
}

View File

@@ -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);