Skip to content

Commit 8059221

Browse files
authored
E2E tests: copy plot to clipboard test (#5474)
Simple check of whether plot copy works by checking the clipboard post click for an image. Also a fix for shiny test. ### QA Notes All smoke tests pass.
1 parent 4c20d05 commit 8059221

File tree

7 files changed

+65
-6
lines changed

7 files changed

+65
-6
lines changed

test/automation/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@ export * from './positron/positronExplorer';
5454
export * from './positron/utils/positronAWSUtils';
5555
export * from './positron/positronQuickaccess';
5656
export * from './positron/positronOutline';
57+
export * from './positron/positronClipboard';
5758
// --- End Positron ---
5859
export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron';

test/automation/src/playwrightDriver.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export class PlaywrightDriver {
3535

3636
constructor(
3737
private readonly application: playwright.Browser | playwright.ElectronApplication,
38-
private readonly context: playwright.BrowserContext,
39-
// --- Start Positron ---
38+
// --- Start Positron --
39+
readonly context: playwright.BrowserContext,
4040
readonly page: playwright.Page,
4141
// --- End Positron ---
4242
private readonly serverProcess: ChildProcess | undefined,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Code } from '../code';
7+
8+
export class PositronClipboard {
9+
10+
constructor(private code: Code) { }
11+
12+
async getClipboardImage(): Promise<Buffer | null> {
13+
// Grant permissions to read from clipboard
14+
await this.code.driver.context.grantPermissions(['clipboard-read']);
15+
16+
const clipboardImageBuffer = await this.code.driver.page.evaluate(async () => {
17+
const clipboardItems = await navigator.clipboard.read();
18+
for (const item of clipboardItems) {
19+
if (item.types.includes('image/png')) {
20+
const blob = await item.getType('image/png');
21+
const arrayBuffer = await blob.arrayBuffer();
22+
return Array.from(new Uint8Array(arrayBuffer));
23+
}
24+
}
25+
return null;
26+
});
27+
28+
return clipboardImageBuffer ? Buffer.from(clipboardImageBuffer) : null;
29+
}
30+
}

test/automation/src/positron/positronPlots.ts

+7
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,11 @@ export class PositronPlots {
9191
async getCurrentStaticPlotAsBuffer(): Promise<Buffer> {
9292
return this.code.driver.getLocator(CURRENT_STATIC_PLOT).screenshot();
9393
}
94+
95+
async copyCurrentPlotToClipboard() {
96+
await this.code.driver.page.locator('.codicon-copy').click();
97+
98+
// wait for clipboard to be populated
99+
await this.code.wait(500);
100+
}
94101
}

test/automation/src/workbench.ts

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { PositronEditor } from './positron/positronEditor';
4646
import { PositronTestExplorer } from './positron/positronTestExplorer';
4747
import { PositronQuickAccess } from './positron/positronQuickaccess';
4848
import { PositronOutline } from './positron/positronOutline';
49+
import { PositronClipboard } from './positron/positronClipboard';
4950
// --- End Positron ---
5051

5152
export interface Commands {
@@ -96,6 +97,7 @@ export class Workbench {
9697
readonly positronTestExplorer: PositronTestExplorer;
9798
readonly positronQuickaccess: PositronQuickAccess;
9899
readonly positronOutline: PositronOutline;
100+
readonly positronClipboard: PositronClipboard;
99101
// --- End Positron ---
100102

101103
constructor(code: Code) {
@@ -141,6 +143,7 @@ export class Workbench {
141143
this.positronTestExplorer = new PositronTestExplorer(code);
142144
this.positronQuickaccess = new PositronQuickAccess(this.quickinput, this.quickaccess);
143145
this.positronOutline = new PositronOutline(code, this.quickaccess);
146+
this.positronClipboard = new PositronClipboard(code);
144147
// --- End Positron ---
145148
}
146149
}

test/smoke/src/areas/positron/apps/shiny.test.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ test.describe('Shiny Application', {
3131
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'shiny-py-example', 'app.py'));
3232
await app.workbench.quickaccess.runCommand('shiny.python.runApp');
3333
const headerLocator = app.workbench.positronViewer.getViewerLocator('h1');
34-
await expect(headerLocator).toHaveText('Restaurant tipping', { timeout: 20000 });
34+
await expect(async () => {
35+
await expect(headerLocator).toHaveText('Restaurant tipping', { timeout: 20000 });
36+
}).toPass({ timeout: 60000 });
3537
});
3638

3739
test('R - Verify Basic Shiny App [C699100]', async function ({ app, r }) {
@@ -40,7 +42,9 @@ runExample("01_hello")`;
4042
await app.workbench.positronConsole.pasteCodeToConsole(code);
4143
await app.workbench.positronConsole.sendEnterKey();
4244
const headerLocator = app.workbench.positronViewer.getViewerLocator('h1');
43-
await expect(headerLocator).toHaveText('Hello Shiny!', { timeout: 20000 });
45+
await expect(async () => {
46+
await expect(headerLocator).toHaveText('Hello Shiny!', { timeout: 20000 });
47+
}).toPass({ timeout: 60000 });
4448
});
4549
});
4650

test/smoke/src/areas/positron/plots/plots.test.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test.describe('Plots', () => {
3030

3131
test('Python - Verifies basic plot functionality - Dynamic Plot [C608114]', {
3232
tag: ['@pr', '@web']
33-
}, async function ({ app, logger }) {
33+
}, async function ({ app, logger, headless }) {
3434
// modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/
3535
logger.log('Sending code to console');
3636
await app.workbench.positronConsole.executeCode('Python', pythonDynamicPlot, '>>>');
@@ -53,6 +53,13 @@ test.describe('Plots', () => {
5353
fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`);
5454
}
5555

56+
if (!headless) {
57+
await app.workbench.positronPlots.copyCurrentPlotToClipboard();
58+
59+
const clipboardImageBuffer = await app.workbench.positronClipboard.getClipboardImage();
60+
expect(clipboardImageBuffer).not.toBeNull();
61+
}
62+
5663
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
5764
await app.workbench.positronPlots.clearPlots();
5865
await app.workbench.positronLayouts.enterLayout('stacked');
@@ -276,7 +283,7 @@ test.describe('Plots', () => {
276283
await interpreter.set('R');
277284
});
278285

279-
test('R - Verifies basic plot functionality [C628633]', { tag: ['@pr', '@web'] }, async function ({ app, logger }) {
286+
test('R - Verifies basic plot functionality [C628633]', { tag: ['@pr', '@web'] }, async function ({ app, logger, headless }) {
280287
logger.log('Sending code to console');
281288
await app.workbench.positronConsole.executeCode('R', rBasicPlot, '>');
282289
await app.workbench.positronPlots.waitForCurrentPlot();
@@ -296,6 +303,13 @@ test.describe('Plots', () => {
296303
fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`);
297304
}
298305

306+
if (!headless) {
307+
await app.workbench.positronPlots.copyCurrentPlotToClipboard();
308+
309+
const clipboardImageBuffer = await app.workbench.positronClipboard.getClipboardImage();
310+
expect(clipboardImageBuffer).not.toBeNull();
311+
}
312+
299313
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
300314
await app.workbench.positronPlots.clearPlots();
301315
await app.workbench.positronLayouts.enterLayout('stacked');

0 commit comments

Comments
 (0)