diff --git a/README.md b/README.md index 6e53a8e..2df5983 100755 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ feel free to open an issue or PR for what you think is missing. | Copy Code to Clipboard | ✅ | ✅ | ❌ | ✅ | | data-testid Selector Support | ✅ | ✅ | ❌ | ✅ | | Text selector support | ❌ | ❌ | ❌ | ✅ | -| Screenshot event generation | ❌ | ✅ | ❌ | ❌ | +| Screenshot event generation | ✅ | ✅ | ❌ | ❌ | | Hover event generation | ✅ | ❌ | ❌ | ❌ | | Record from Chrome Stable | ✅ | ✅ | ❌ | ❌ | diff --git a/package.json b/package.json index 76176d4..8ca0948 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "deploysentinel-recorder", - "version": "0.1.0", + "version": "0.1.1", "description": "A Chrome Extension that generates Playwright and Puppeteer scripts automatically from your browser interactions.", "license": "Apache-2.0", "scripts": { diff --git a/src/pages/Common/styles.css b/src/pages/Common/styles.css index 5905249..8e4ab8b 100644 --- a/src/pages/Common/styles.css +++ b/src/pages/Common/styles.css @@ -137,6 +137,9 @@ .mr-2 { margin-right: 0.5em; } +.mr-4 { + margin-right: 1em; +} .mb-2 { margin-bottom: 0.5em; } diff --git a/src/pages/Content/ActionList.tsx b/src/pages/Content/ActionList.tsx index 7b889df..a597648 100644 --- a/src/pages/Content/ActionList.tsx +++ b/src/pages/Content/ActionList.tsx @@ -78,6 +78,10 @@ function ActionListItem({ X:{action.deltaX}, Y:{action.deltaY} + ) : action.type === 'fullScreenshot' ? ( + <> + Take full page screenshot + ) : ( <> )} @@ -89,7 +93,7 @@ export default function ActionList({ actions }: { actions: Action[] }) { return ( <> -
+
{actions .filter((action) => [ @@ -100,6 +104,7 @@ export default function ActionList({ actions }: { actions: Action[] }) { 'load', 'resize', 'wheel', + 'fullScreenshot', ].includes(action.type) ) .map((action, i) => ( diff --git a/src/pages/Content/CodeGen.tsx b/src/pages/Content/CodeGen.tsx index 701d6eb..74bf7eb 100644 --- a/src/pages/Content/CodeGen.tsx +++ b/src/pages/Content/CodeGen.tsx @@ -77,6 +77,10 @@ ${lines} deltaY )});`; } + + fullScreenshot() { + return `await page.screenshot({ path: 'screenshot.png', fullPage: true });`; + } } class PuppeteerScriptBuilder { @@ -159,6 +163,10 @@ ${lines} wheel(deltaX: number, deltaY: number) { return `await page.evaluate(() => window.scrollBy(${deltaX}, ${deltaY}));`; } + + fullScreenshot() { + return `await page.screenshot({ path: 'screenshot.png', fullPage: true });`; + } } function describeAction(action: Action) { @@ -192,6 +200,8 @@ function describeAction(action: Action) { ? `Resize window to ${action.width} x ${action.height}` : action.type === 'wheel' ? `Scroll wheel by X:${action.deltaX}, Y:${action.deltaY}` + : action.type === 'fullScreenshot' + ? `Take full page screenshot` : ''; } @@ -255,6 +265,8 @@ export function genCode( line += scriptBuilder.resize(action.width, action.height); } else if (action.type === 'wheel') { line += scriptBuilder.wheel(action.deltaX, action.deltaY); + } else if (action.type === 'fullScreenshot') { + line += scriptBuilder.fullScreenshot(); } else { return null; } diff --git a/src/pages/Content/ControlBar.tsx b/src/pages/Content/ControlBar.tsx index d5cfa32..b2fca86 100644 --- a/src/pages/Content/ControlBar.tsx +++ b/src/pages/Content/ControlBar.tsx @@ -4,6 +4,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { + faCamera, faCopy, faCheck, faCheckCircle, @@ -26,12 +27,14 @@ const ActionButton = ({ onClick, children, label, + testId, }: { onClick: () => void; children: JSX.Element; label: String; + testId?: String; }) => ( -
+
); @@ -102,6 +107,7 @@ export default function ControlBar({ onExit }: { onExit: () => void }) { 'actions' | 'playwright' | 'puppeteer' >('playwright'); const [copyCodeConfirm, setCopyCodeConfirm] = useState(false); + const [screenshotConfirm, setScreenshotConfirm] = useState(false); const [isFinished, setIsFinished] = useState(false); @@ -208,7 +214,10 @@ export default function ControlBar({ onExit }: { onExit: () => void }) {
- Recording Finished!🎉 + + Recording Finished! + + 🎉
onClose()}> @@ -233,7 +242,11 @@ export default function ControlBar({ onExit }: { onExit: () => void }) {
) : (
- onEndRecording()}> + onEndRecording()} + testId="end-test" + >
@@ -254,9 +267,12 @@ export default function ControlBar({ onExit }: { onExit: () => void }) {
setShowAllActions(!showAllActions)} > - {showAllActions ? 'Hide' : 'See'} Prior Steps{' '} + {showAllActions ? 'Show Less' : 'Show More'}{' '} @@ -271,58 +287,78 @@ export default function ControlBar({ onExit }: { onExit: () => void }) {
{ setShowActionsMode( showActionsMode === 'actions' ? 'playwright' : 'actions' ); }} > - Show{' '} - {showActionsMode === 'actions' ? 'Generated Code' : 'Actions'} + Show {showActionsMode === 'actions' ? 'Code' : 'Actions'} + + { + recorderRef.current?.onFullScreenshot(); + setScreenshotConfirm(true); + setTimeout(() => { + setScreenshotConfirm(false); + }, 2000); + }} + > + {' '} + Record Screenshot - {(showActionsMode === 'playwright' || - showActionsMode === 'puppeteer') && ( - { - setShowActionsMode( - showActionsMode === 'playwright' - ? 'puppeteer' - : 'playwright' - ); - }} - > - Switch to{' '} - {showActionsMode === 'playwright' - ? 'Puppeteer' - : 'Playwright'} - - )}
{(showActionsMode === 'playwright' || showActionsMode === 'puppeteer') && ( - { - setCopyCodeConfirm(true); - setTimeout(() => { - setCopyCodeConfirm(false); - }, 2000); - }} - > + <> { + setShowActionsMode( + showActionsMode === 'playwright' + ? 'puppeteer' + : 'playwright' + ); + }} > - {' '} - Copy Code + Switch to{' '} + {showActionsMode === 'playwright' + ? 'Puppeteer' + : 'Playwright'} - + { + setCopyCodeConfirm(true); + setTimeout(() => { + setCopyCodeConfirm(false); + }, 2000); + }} + > + + {' '} + Copy Code + + + )}
diff --git a/src/pages/Content/recorder.ts b/src/pages/Content/recorder.ts index 09f00a2..b0de1a6 100644 --- a/src/pages/Content/recorder.ts +++ b/src/pages/Content/recorder.ts @@ -36,7 +36,8 @@ export type Action = source: string; } | ResizeAction - | { type: 'wheel'; deltaY: number; deltaX: number }; + | { type: 'wheel'; deltaY: number; deltaX: number } + | { type: 'fullScreenshot' }; function isEventFromOverlay(event: Event) { return ( @@ -308,6 +309,7 @@ class Recorder { timestamp: event.timeStamp, }; + // @ts-ignore this.appendToRecording(action); }; @@ -319,7 +321,7 @@ class Recorder { lastResizeAction.width !== width || lastResizeAction.height !== height ) { - const action = { + const action: Action = { type: 'resize', width, height, @@ -341,6 +343,14 @@ class Recorder { }; private debouncedOnResize = debounce(this.onResize, 300); + + public onFullScreenshot = (): void => { + const action: Action = { + type: 'fullScreenshot', + }; + + this.appendToRecording(action); + }; } export default Recorder; diff --git a/tests/extension.test.ts b/tests/extension.test.ts index 718c638..58e26e1 100644 --- a/tests/extension.test.ts +++ b/tests/extension.test.ts @@ -1,7 +1,7 @@ import playwright from 'playwright'; import type { BrowserContext, Worker } from 'playwright'; -jest.setTimeout(10000); +jest.setTimeout(15000); function waitForServiceWorkers( browser: BrowserContext, @@ -27,7 +27,7 @@ function waitForServiceWorkers( let extensionId = ''; let browserContext: BrowserContext | null = null; beforeAll(async () => { - const userDataDir = '/tmp/test-user-data-dir'; + const userDataDir = '/tmp/deploysentinel-recorder-test-user-data-dir'; browserContext = await playwright['chromium'].launchPersistentContext( userDataDir, { @@ -52,7 +52,7 @@ test('extension is installed', async () => { expect(extensionId).toBeTruthy(); }); -test('can click through recording steps', async () => { +test('control bar shows correct actions during recording', async () => { expect(browserContext).toBeTruthy(); if (browserContext == null) { return; @@ -69,6 +69,56 @@ test('can click through recording steps', async () => { await page.press('#searchInput', 'Enter', { delay: 100 }); await page.click('[href="/wiki/Corn_tortilla"]:nth-child(7)'); + // Take Screenshot + await page.click('[data-testid="show-more-actions"]'); + await page.click('[data-testid="record-screenshot"]'); + + // Show actions in control bar + await page.click('[data-testid="show-actions"]'); + + const content = await page.textContent('[data-testid="action-list"]'); + expect(content).toEqual( + expect.stringContaining('Click on input #searchInput') + ); + expect(content).toEqual( + expect.stringContaining('Fill "tacos" on #searchInput') + ); + expect(content).toEqual( + expect.stringContaining('Press "Enter" on #searchInput') + ); + expect(content).toEqual(expect.stringContaining('Click on link "corn"')); + expect(content).toEqual(expect.stringContaining('Take full page screenshot')); + + // End test + await page.click('[data-testid="end-test"]'); + expect(await page.textContent('[data-testid="recording-finished"]')).toEqual( + 'Recording Finished!' + ); + + await page.close(); +}); + +test('can click through recording steps and it generates the right code', async () => { + expect(browserContext).toBeTruthy(); + if (browserContext == null) { + return; + } + + const page = await browserContext.newPage(); + await page.goto(`chrome-extension://${extensionId}/popup.html`); + + await page.click('[data-testid="record-new-test"]'); + + await page.goto('https://wikipedia.com'); + await page.click('#searchInput'); + await page.type('#searchInput', 'tacos', { delay: 100 }); + await page.press('#searchInput', 'Enter', { delay: 100 }); + await page.click('[href="/wiki/Corn_tortilla"]:nth-child(7)'); + + // Take Screenshot + await page.click('[data-testid="show-more-actions"]'); + await page.click('[data-testid="record-screenshot"]'); + await page.goto(`chrome-extension://${extensionId}/popup.html`); await page.click('[data-testid="end-test-recording"]'); await page.click('[data-testid="view-last-test"]'); @@ -91,4 +141,11 @@ test('can click through recording steps', async () => { 'page.click(\'[href="/wiki/Corn_tortilla"]:nth-child(7)\'),' ) ); + expect(content).toEqual( + expect.stringContaining( + "await page.screenshot({ path: 'screenshot.png', fullPage: true });" + ) + ); + + await page.close(); });