Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

swiping through images in viewer widget #117

Merged
merged 29 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3e90529
swiping through images
JakubSerafin Dec 13, 2023
d94c4bf
first passing test
JakubSerafin Dec 15, 2023
768f034
Merge remote-tracking branch 'origin/master' into feature/image-viewe…
JakubSerafin Jan 23, 2024
670eeba
tree first test working
JakubSerafin Jan 24, 2024
3149f7c
Refactor widget setup and add custom widget mapping
JakubSerafin Jan 24, 2024
e0f5047
tests for empty image
JakubSerafin Jan 24, 2024
d3e4dee
Add image navigation functionality tests with multimage scenario
JakubSerafin Jan 24, 2024
a90ac07
swype tests
JakubSerafin Jan 25, 2024
de1f7c7
Remove redundant CSS styles commited by mistake, a bit of ode cleanup…
JakubSerafin Jan 26, 2024
70d6f53
Update package.json dependencies
JakubSerafin Jan 26, 2024
07e8530
post code review fixes
JakubSerafin Feb 5, 2024
e7783f2
Fix missing newline at end of file in script.js
JakubSerafin Feb 5, 2024
ec38efc
Add styling to button and body
JakubSerafin Feb 5, 2024
93465ba
styuling improvments and hidding buttons when touchscreen (mobile) is…
JakubSerafin Feb 5, 2024
41915f4
Update drop-shadow filter in index.html
JakubSerafin Feb 5, 2024
9907c21
refactor undo method in gristWebDriverUtils.ts
JakubSerafin Feb 5, 2024
45237dc
Add lodash.escaperegexp dependencies
JakubSerafin Feb 5, 2024
0e2b751
Add/update devDependencies in package.json
JakubSerafin Feb 5, 2024
93ea31c
set repository to registry.yarnpkg.com
JakubSerafin Feb 5, 2024
2845dba
linting, formating
JakubSerafin Feb 8, 2024
d7494f4
fixing problem with mocha firefox headless
paulfitz Nov 28, 2023
47aeea5
Update test/init-mocha-webdriver.ts
JakubSerafin Feb 14, 2024
fea7c44
test for switching diffrent state of imageviewer
JakubSerafin Feb 14, 2024
20432bf
refactor
JakubSerafin Feb 14, 2024
a39429c
more refactor
JakubSerafin Feb 14, 2024
06eed2c
even more refactor
JakubSerafin Feb 14, 2024
e8a08bc
navigation button will disapear when navigating from mult image to none
JakubSerafin Feb 14, 2024
2cbc719
Merge remote-tracking branch 'origin/feature/image-viewer-multi-image…
JakubSerafin Feb 14, 2024
9d8a672
cursor change to pointer on image navigation buttons, also test focus…
JakubSerafin Feb 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
},
"devDependencies": {
"@types/chai": "^4.3.5",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/mocha": "^10.0.1",
"@types/node": "18.11.9",
"@types/node-fetch": "^2.6.4",
"@types/selenium-webdriver": "^4.1.15",
"commander": "^11.1.0",
"live-server": "^1.2.1",
"lodash": "^4.17.21",
"lodash.escaperegexp": "^4.1.2",
"mocha": "^10.2.0",
"mocha-webdriver": "0.3.1",
"node-fetch": "^2",
Expand Down
Binary file added test/fixtures/docs/Images.grist
Binary file not shown.
Binary file added test/fixtures/images/image1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/images/image2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/images/image3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 20 additions & 16 deletions test/getGrist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import fetch from 'node-fetch';
import {GristWebDriverUtils} from 'test/gristWebDriverUtils';


type UserAction = Array<string | number | object | boolean | null | undefined>;


/**
* Set up mocha hooks for starting and stopping Grist. Return
Expand Down Expand Up @@ -198,26 +198,12 @@ export class GristUtils extends GristWebDriverUtils {
await this.waitForServer();
}

public async sendActionsAndWaitForServer(actions: UserAction[], optTimeout: number = 2000) {
const result = await driver.executeAsyncScript(async (actions: any, done: Function) => {
try {
await (window as any).gristDocPageModel.gristDoc.get().docModel.docData.sendActions(actions);
done(null);
} catch (err) {
done(String(err?.message || err));
}
}, actions);
if (result) {
throw new Error(result as string);
}
await this.waitForServer(optTimeout);
}


public async clickWidgetPane() {
const elem = this.driver.find('.test-config-widget-select .test-select-open');
if (await elem.isPresent()) {
await elem.click();
// if not present, may just be already selected.
}
}

Expand All @@ -226,6 +212,20 @@ export class GristUtils extends GristWebDriverUtils {
await this.waitForServer();
}

public async removeWidget(name: string|RegExp) {
await this.selectSectionByTitle(name);
await this.sendCommand('deleteSection');
await this.waitForServer();
}

public async addCustomSection(name: string, type: string, dataSource: string|RegExp= /Table1/) {
await this.toggleSidePanel('right', 'open');
await this.addNewSection(/Custom/, dataSource);
await this.clickWidgetPane();
await this.selectCustomWidget(type);
await this.waitForServer();
}

public async setCustomWidgetAccess(option: "none" | "read table" | "full") {
const text = {
"none": "No document access",
Expand Down Expand Up @@ -275,6 +275,10 @@ export class GristUtils extends GristWebDriverUtils {
return this.inCustomWidget(() => this.driver.find(selector).getText());
}

public async getCustomWidgetElementParameter(selector: string, parameter: string): Promise<string> {
return this.inCustomWidget(() => this.driver.find(selector).getAttribute(parameter));
}

public async executeScriptInCustomWidget<T>(script: Function, ...args: any[]): Promise<T> {
return this.inCustomWidget(() => {
return driver.executeScript(script, ...args);
Expand Down
204 changes: 185 additions & 19 deletions test/gristWebDriverUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
* easily.
*/

import { WebDriver, WebElement } from 'mocha-webdriver';
import { Key, WebDriver, WebElement, WebElementPromise } from 'mocha-webdriver';
import escapeRegExp = require('lodash/escapeRegExp');

type SectionTypes = 'Table'|'Card'|'Card List'|'Chart'|'Custom';
type SectionTypes = 'Table' | 'Card' | 'Card List' | 'Chart' | 'Custom';
type UserAction = Array<string | number | object | boolean | null | undefined>;

export class GristWebDriverUtils {
public constructor(public driver: WebDriver) {
}

public isSidePanelOpen(which: 'right'|'left'): Promise<boolean> {
public isSidePanelOpen(which: 'right' | 'left'): Promise<boolean> {
return this.driver.find(`.test-${which}-panel`).matches('[class*=-open]');
}

Expand All @@ -31,14 +33,44 @@ export class GristWebDriverUtils {
public async waitForServer(optTimeout: number = 2000) {
await this.driver.wait(() => this.driver.executeScript(
"return window.gristApp && (!window.gristApp.comm || !window.gristApp.comm.hasActiveRequests())"
+ " && window.gristApp.testNumPendingApiRequests() === 0",
+ " && window.gristApp.testNumPendingApiRequests() === 0",
optTimeout,
"Timed out waiting for server requests to complete"
));
}

public async sendActionsAndWaitForServer(actions: UserAction[], optTimeout: number = 2000) {
const result = await this.driver.executeAsyncScript(async (actions: any, done: Function) => {
try {
await (window as any).gristDocPageModel.gristDoc.get().docModel.docData.sendActions(actions);
done(null);
} catch (err) {
done(String(err?.message || err));
}
}, actions);
if (result) {
throw new Error(result as string);
}
await this.waitForServer(optTimeout);
}

/**
* Runs a Grist command in the browser window.
*/
public async sendCommand(name: string, argument: any = null) {
await this.driver.executeAsyncScript((name: any, argument: any, done: any) => {
const result = (window as any).gristApp.allCommands[name].run(argument);
if (result?.finally) {
result.finally(done);
} else {
done();
}
}, name, argument);
await this.waitForServer();
}


public async login(){
public async login() {
//just click log in to get example account.
const menu = await this.driver.findWait('.test-dm-account', 1000);
await menu.click();
Expand All @@ -50,7 +82,7 @@ export class GristWebDriverUtils {

public async waitForSidePanel() {
// 0.4 is the duration of the transition setup in app/client/ui/PagePanels.ts for opening the
// side panes
// side panes
const transitionDuration = 0.4;

// let's add an extra delay of 0.1 for even more robustness
Expand All @@ -62,7 +94,7 @@ export class GristWebDriverUtils {
* Toggles (opens or closes) the right or left panel and wait for the transition to complete. An optional
* argument can specify the desired state.
*/
public async toggleSidePanel(which: 'right'|'left', goal: 'open'|'close'|'toggle' = 'toggle') {
public async toggleSidePanel(which: 'right' | 'left', goal: 'open' | 'close' | 'toggle' = 'toggle') {
if ((goal === 'open' && await this.isSidePanelOpen(which)) ||
(goal === 'close' && !await this.isSidePanelOpen(which))) {
return;
Expand All @@ -80,14 +112,14 @@ export class GristWebDriverUtils {
* Gets browser window dimensions.
*/
public async getWindowDimensions(): Promise<WindowDimensions> {
const {width, height} = await this.driver.manage().window().getRect();
return {width, height};
const { width, height } = await this.driver.manage().window().getRect();
return { width, height };
}


// Add a new widget to the current page using the 'Add New' menu.
public async addNewSection(
typeRe: RegExp|SectionTypes, tableRe: RegExp|string, options?: PageWidgetPickerOptions
typeRe: RegExp | SectionTypes, tableRe: RegExp | string, options?: PageWidgetPickerOptions
) {
// Click the 'Add widget to page' entry in the 'Add New' menu
await this.driver.findWait('.test-dp-add-new', 2000).doClick();
Expand All @@ -100,8 +132,8 @@ export class GristWebDriverUtils {
// Select type and table that matches respectively typeRe and tableRe and save. The widget picker
// must be already opened when calling this function.
public async selectWidget(
typeRe: RegExp|string,
tableRe: RegExp|string = '',
typeRe: RegExp | string,
tableRe: RegExp | string = '',
options: PageWidgetPickerOptions = {}
) {
const driver = this.driver;
Expand Down Expand Up @@ -229,9 +261,9 @@ export class GristWebDriverUtils {
await this.openAccountMenu();
await this.driver.find('.grist-floating-menu .test-dm-account-settings').click();
//close alert if it is shown
if(await this.isAlertShown()){
if (await this.isAlertShown()) {
await this.acceptAlert();
};
}
await this.driver.findWait('.test-account-page-login-method', 5000);
await this.waitForServer();
return new ProfileSettingsPage(this);
Expand Down Expand Up @@ -300,12 +332,122 @@ export class GristWebDriverUtils {
let oldDimensions: WindowDimensions;
before(async () => {
oldDimensions = await this.driver.manage().window().getRect();
await this.driver.manage().window().setRect({width: 1920, height: 1080});
await this.driver.manage().window().setRect({ width: 1920, height: 1080 });
});
after(async () => {
await this.driver.manage().window().setRect(oldDimensions);
});
}

public async focusOnCell(columnName: string, row: number) {
const cell = await this.getCell({ col: columnName, rowNum: row });
await cell.click();
}
public async fillCell(columnName: string, row: number, value: string) {
await this.focusOnCell(columnName, row);
await this.driver.sendKeys(value)
await this.driver.sendKeys(Key.ENTER);
}

public async addColumn(table: string, name: string) {
// focus on table
await this.selectSectionByTitle(table);
// add new column using a shortcut
await this.driver.actions().keyDown(Key.ALT).sendKeys('=').keyUp(Key.ALT).perform();
// wait for rename panel to show up
await this.driver.findWait('.test-column-title-popup', 1000);
// rename and accept
await this.driver.sendKeys(name);
await this.driver.sendKeys(Key.ENTER);
await this.waitForServer();
}

/**
* Click into a section without disrupting cursor positions.
*/
public async selectSectionByTitle(title: string|RegExp) {
try {
if (typeof title === 'string') {
title = new RegExp("^" + escapeRegExp(title) + "$", 'i');
}
// .test-viewsection is a special 1px width element added for tests only.
await this.driver.findContent(`.test-viewsection-title`, title).find(".test-viewsection-blank").click();
} catch (e) {
// We might be in mobile view.
await this.driver.findContent(`.test-viewsection-title`, title).findClosest(".view_leaf").click();
}
}


/**
* Returns a visible GridView cell. Options may be given as arguments directly, or as an object.
* - col: column name, or 0-based column index
* - rowNum: 1-based row numbers, as visible in the row headers on the left of the grid.
* - section: optional name of the section to use; will use active section if omitted.
*/
public getCell(col: number | string, rowNum: number, section?: string): WebElementPromise;
public getCell(options: ICellSelect): WebElementPromise;
public getCell(colOrOptions: number | string | ICellSelect, rowNum?: number, section?: string): WebElementPromise {
const mapper = async (el: WebElement) => el;
const options: IColSelect<WebElement> = (typeof colOrOptions === 'object' ?
{ col: colOrOptions.col, rowNums: [colOrOptions.rowNum], section: colOrOptions.section, mapper } :
{ col: colOrOptions, rowNums: [rowNum!], section, mapper });
return new WebElementPromise(this.driver, this.getVisibleGridCells(options).then((elems) => elems[0]));
}

/**
* Returns visible cells of the GridView from a single column and one or more rows. Options may be
* given as arguments directly, or as an object.
* - col: column name, or 0-based column index
* - rowNums: array of 1-based row numbers, as visible in the row headers on the left of the grid.
* - section: optional name of the section to use; will use active section if omitted.
*
* If given by an object, then an array of columns is also supported. In this case, the return
* value is still a single array, listing all values from the first row, then the second, etc.
*
* Returns cell text by default. Mapper may be `identity` to return the cell objects.
*/
public async getVisibleGridCells(col: number | string, rows: number[], section?: string): Promise<string[]>;
public async getVisibleGridCells<T = string>(options: IColSelect<T> | IColsSelect<T>): Promise<T[]>;
public async getVisibleGridCells<T>(
colOrOptions: number | string | IColSelect<T> | IColsSelect<T>, _rowNums?: number[], _section?: string
): Promise<T[]> {

if (typeof colOrOptions === 'object' && 'cols' in colOrOptions) {
const { rowNums, section, mapper } = colOrOptions; // tslint:disable-line:no-shadowed-variable
const columns = await Promise.all(colOrOptions.cols.map((oneCol) =>
this.getVisibleGridCells({ col: oneCol, rowNums, section, mapper })));
// This zips column-wise data into a flat row-wise array of values.
return ([] as T[]).concat(...rowNums.map((r, i) => columns.map((c) => c[i])));
}

const { col, rowNums, section, mapper = el => el.getText() }: IColSelect<any> = (
typeof colOrOptions === 'object' ? colOrOptions :
{ col: colOrOptions, rowNums: _rowNums!, section: _section }
);

if (rowNums.includes(0)) {
// Row-numbers should be what the users sees: 0 is a mistake, so fail with a helpful message.
throw new Error('rowNum must not be 0');
}

const sectionElem = section ? await this.getSection(section) : await this.driver.findWait('.active_section', 4000);
const colIndex = (typeof col === 'number' ? col :
await sectionElem.findContent('.column_name', exactMatch(col)).index());

const visibleRowNums: number[] = await sectionElem.findAll('.gridview_data_row_num',
async (el) => parseInt(await el.getText(), 10));

const selector = `.gridview_data_scroll .record:not(.column_names) .field:nth-child(${colIndex + 1})`;
const fields = mapper ? await sectionElem.findAll(selector, mapper) : await sectionElem.findAll(selector);
return rowNums.map((n) => fields[visibleRowNums.indexOf(n)]);
}

public getSection(sectionOrTitle: string | WebElement): WebElement | WebElementPromise {
if (typeof sectionOrTitle !== 'string') { return sectionOrTitle; }
return this.driver.findContent(`.test-viewsection-title`, new RegExp("^" + escapeRegExp(sectionOrTitle) + "$", 'i'))
.findClosest('.viewsection_content');
}
}

class ProfileSettingsPage {
Expand All @@ -318,7 +460,7 @@ class ProfileSettingsPage {
}

public async setLanguage(language: string) {
await this.driver.findWait('.test-account-page-language .test-select-open',100).click();
await this.driver.findWait('.test-account-page-language .test-select-open', 100).click();
await this.driver.findContentWait('.test-select-menu li', language, 100).click();
await this.gu.waitForServer();
}
Expand All @@ -332,11 +474,35 @@ export interface WindowDimensions {
export interface PageWidgetPickerOptions {
tableName?: string;
/** Optional pattern of SELECT BY option to pick. */
selectBy?: RegExp|string;
selectBy?: RegExp | string;
/** Optional list of patterns to match Group By columns. */
summarize?: (RegExp|string)[];
summarize?: (RegExp | string)[];
/** If true, configure the widget selection without actually adding to the page. */
dontAdd?: boolean;
/** If true, dismiss any tooltips that are shown. */
dismissTips?: boolean;
}
}

export interface IColsSelect<T = WebElement> {
cols: Array<number | string>;
rowNums: number[];
section?: string | WebElement;
mapper?: (e: WebElement) => Promise<T>;
}

export interface IColSelect<T = WebElement> {
col: number | string;
rowNums: number[];
section?: string | WebElement;
mapper?: (e: WebElement) => Promise<T>;
}

export interface ICellSelect {
col: number | string;
rowNum: number;
section?: string | WebElement;
}

export function exactMatch(value: string, flags?: string): RegExp {
return new RegExp(`^${escapeRegExp(value)}$`, flags);
}
Loading
Loading