Skip to content

Commit

Permalink
fixed calculation of absolute and relative path dynamic imports
Browse files Browse the repository at this point in the history
make live recorder work when test run with `playwright test --ui`
expose .d.ts types properly
pageObjectModel - track and reload only the current file, no deps tracking needed
better cleanup of highlighted page object model elements
better exposing of selectorProperties and helperMethods from page object model files
  • Loading branch information
michaelgoeke committed Jan 14, 2025
1 parent 3aa6ec0 commit bcbd7cf
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 133 deletions.
25 changes: 12 additions & 13 deletions src/browser/PW_live.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function updateTooltipPosition(x, y) {

window.mousemove_updateToolTip_running = false;
function mousemove_updateTooltip(event) {
if (mousemove_updateToolTip_running === true) return; //exit early so we don't sawmp the CPU
if (mousemove_updateToolTip_running === true) return; //exit early so we don't swamp the CPU
try {
mousemove_updateToolTip_running = true;
const element = document.elementFromPoint(event.x, event.y);
Expand Down Expand Up @@ -185,7 +185,7 @@ window.addEventListener("click", recordModeClickHandler, true);
/******** page object model feature ********/

window.navigation.onnavigatesuccess = async () => await reload_page_object_model_elements();
window.setInterval(async () => await reload_page_object_model_elements(), 5000); //refresh the page object model highlighting every 5 seconds in case on-screen elements have changed
//window.setInterval(async () => await reload_page_object_model_elements(), 5000); //refresh the page object model highlighting every 5 seconds in case on-screen elements have changed


var pageObjectFilePath = "";
Expand All @@ -202,28 +202,21 @@ async function reload_page_object_model_elements() {
const pageObject = window.PW_pages[pageObjectFilePath];
if (pageObject === undefined) return;

const propertyRegex = new RegExp(config.pageObjectModel.propertySelectorRegex.slice(1, -1));
const pageObjectModelImportStatement = await PW_importStatement(pageObject.className, pageObjectFilePath);
for (var prop in pageObject.page) {
for (var prop of pageObject.selectors) {
try {
const selectorMethodName = propertyRegex.exec(prop)?.[1];
if (!selectorMethodName) continue;

const selector = pageObject.page[prop];
const matchingElements = playwright.locator(selector).elements;
const matchingElements = playwright.locator(prop.selector).elements;
if (matchingElements.length > 1) {
//todo: show a warning somehow
}
if (matchingElements.length === 0) {
console.info(`could not find element for selector ${selector}. skipping.`);
console.info(`could not find element for selector ${prop.selector}. skipping.`);
continue;
}

const selectorMethod = "" + pageObject.page[selectorMethodName].toString();
const selectorMethodArgs = selectorMethod.slice(selectorMethod.indexOf("("), selectorMethod.indexOf(")") + 1);
const primaryAction = config.pageObjectModel.primaryActionByCssSelector.find(([css]) => matchingElements[0].matches(css))[1];
const secondaryActions = config.pageObjectModel.secondaryActionByCssSelector.filter(([css]) => matchingElements[0].matches(css)).map(([, action]) => action);
const dataPageObjectModel = `${pageObject.className}.${selectorMethodName}${selectorMethodArgs}`;
const dataPageObjectModel = `${pageObject.className}.${prop.selectorMethod.name}(${prop.selectorMethod.args.join(', ')})`;
for (const el of matchingElements) {
el.setAttribute("data-page-object-model", dataPageObjectModel);
el.setAttribute("data-page-object-model-import", pageObjectModelImportStatement);
Expand All @@ -241,6 +234,12 @@ async function reload_page_object_model_elements() {

function clearPageObjectModelElements() {
if (window.PW_overlays !== undefined) for (const el of window.PW_overlays) config.pageObjectModel.overlay.off(el);
//clean up any rogue elements
const pageObjectModelAttributes = ['data-page-object-model', 'data-page-object-model-import', 'data-page-object-model-primary-action', 'data-page-object-model-secondary-actions'];
document.querySelectorAll(pageObjectModelAttributes.join(', ')).forEach(el => {
pageObjectModelAttributes.forEach(attr => el.removeAttribute(attr));
config.pageObjectModel.overlay.off(el)
});
window.PW_overlays = [];
}

Expand Down
14 changes: 8 additions & 6 deletions src/hotModuleReload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ export module hotModuleReload {
const blockToExecute = _getBlockToExecute(s.testFnContents, newTestFnContents);
if (blockToExecute === '')
return;
await evalLines(blockToExecute, s);
await evalLines(blockToExecute);
s.testFnContents = newTestFnContents;
} finally {
release();
}
});
}

async function evalLines(lines: string, s: hotModuleReloadState) {
const importsBlock = _rewriteAsDynamicImports(s.imports).join('\n');
async function evalLines(lines: string) {
const importsBlock = _rewriteAsDynamicImports(_state.imports).join('\n');
const wrappedEvalLines = _wrapAsyncAsPromise(importsBlock + '\n\n' + lines, _extractVariableListFrom(lines));
await _evalCore(s.evalScope, s.pageEvaluate, [wrappedEvalLines]);
return _evalCore(_state.evalScope, _state.pageEvaluate, wrappedEvalLines);
}

function _rewriteAsDynamicImports(imports: ImportDeclaration[])
Expand Down Expand Up @@ -91,10 +91,11 @@ ${variables.length === 0 ? `` : `Object.assign(globalThis, { ${variables.join(',
return variableNames.flat();
}

export async function _evalCore(evalScope: (s: string) => any, pageEvaluate: (pageFunction: string) => Promise<unknown>, codeBlocks: string[]) {
export async function _evalCore(evalScope: (s: string) => any, pageEvaluate: (pageFunction: string) => Promise<unknown>, codeBlock: string) {
let result;
try {
await pageEvaluate(`window.PW_executing = true`);
await evalScope(codeBlocks.join(''));
result = await evalScope(codeBlock);
await pageEvaluate(`PW_reportError()`);
} catch (error) {
if (error instanceof Error) {
Expand All @@ -107,6 +108,7 @@ ${variables.length === 0 ? `` : `Object.assign(globalThis, { ${variables.join(',
} finally {
await pageEvaluate(`window.PW_executing = false; window.reload_page_object_model_elements();`);
}
return result;
}

export function _extractImports(filename: string) {
Expand Down
16 changes: 8 additions & 8 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { hotModuleReload } from "./hotModuleReload";
import { pageObjectModel } from "./pageObjectModel";
import { getTestCallingLocation } from "./utility";
import fs from 'fs/promises';
import process from 'node:process';
import { ts } from "ts-morph";
import { PlaywrightLiveRecorderConfig, PlaywrightLiveRecorderConfigFile } from "./types";

//github copilot directive: for the defaultConfig property - add inline jsdoc @default attributes for each property below, have the @default value match the actual values of each property
import { PlaywrightLiveRecorderConfig, PlaywrightLiveRecorderConfigFile, PlaywrightLiveRecorderConfig_recorder, PlaywrightLiveRecorderConfig_pageObjectModel, PlaywrightLiveRecorderConfig_diagnostic, TestCallingLocation } from "./types";
export { PlaywrightLiveRecorderConfig, PlaywrightLiveRecorderConfigFile, PlaywrightLiveRecorderConfig_recorder, PlaywrightLiveRecorderConfig_pageObjectModel, PlaywrightLiveRecorderConfig_diagnostic, TestCallingLocation };

export module PlaywrightLiveRecorder {
export const defaultConfig: PlaywrightLiveRecorderConfig = {
Expand Down Expand Up @@ -126,11 +126,11 @@ export class ${className} {
pageState.PlaywrightLiveRecorder_started = true;

const isHeadless = test.info().project.use.headless;
if (isHeadless !== false) {
console.error('startLiveCoding called while running headless');
const pwdebug = process.env.PWDEBUG == 'console';
if (isHeadless !== false && !pwdebug) {
console.error('startLiveCoding called while running headless or env variable PWDEBUG=console not set');
return;
}

config = _mergeConfig(defaultConfig, await _configFromFile(), configOverrides);
if (!config.pageObjectModel.path.endsWith('/')) config.pageObjectModel.path +='/';

Expand All @@ -140,7 +140,7 @@ export class ${className} {
await testFileWriter.init(page, testCallingLocation);

await hotModuleReload.init(testCallingLocation, config.pageObjectModel.importerCustomizationHooks, (str: string) => page.evaluate(str), evalScope);
await page.exposeFunction('PW_eval', (codeBlocks: string[]) => hotModuleReload._evalCore(evalScope, s => page.evaluate(s), codeBlocks));
await page.exposeFunction('PW_eval', (codeBlock: string) => hotModuleReload._evalCore(evalScope, s => page.evaluate(s), codeBlock));

await recorder.init(config.recorder, page);

Expand All @@ -150,7 +150,7 @@ export class ${className} {

if (config.pageObjectModel.enabled) {
config.pageObjectModel.baseUrl = config.pageObjectModel.baseUrl ?? test.info().project.use.baseURL!;
await pageObjectModel.init(nodePath.dirname(testCallingLocation.file), config.pageObjectModel, page);
await pageObjectModel.init(nodePath.dirname(testCallingLocation.file), config.pageObjectModel, evalScope, page);
}

page.on('load', async page => {
Expand Down
Loading

0 comments on commit bcbd7cf

Please sign in to comment.