Skip to content

Commit e760c4c

Browse files
committed
(kb) add a first batch of simple tests for the RegionFocusSwitcher
more advanced tests are missing, but this is a good start
1 parent 9361e98 commit e760c4c

File tree

4 files changed

+312
-2
lines changed

4 files changed

+312
-2
lines changed

app/client/components/RegionFocusSwitcher.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ export class RegionFocusSwitcher extends Disposable {
217217
* - make sure the internal current region info is set when user clicks on the view layout.
218218
*/
219219
private _onClick(event: MouseEvent) {
220-
const current = this._state.get().region;
221220
const gristDoc = this._getGristDoc();
222221
if (!gristDoc) {
223222
return;
@@ -228,8 +227,11 @@ export class RegionFocusSwitcher extends Disposable {
228227
}
229228
const targetRegionId = closestRegion.getAttribute(ATTRS.regionId);
230229
const targetsMain = targetRegionId === 'main';
230+
const current = this._state.get().region;
231231
const currentlyInSection = current?.type === 'section';
232232

233+
console.log('mhl onClick', {event, closestRegion, targetRegionId, current, currentlyInSection});
234+
233235
if (targetsMain && !currentlyInSection) {
234236
this.focusRegion(
235237
{type: 'section', id: gristDoc.viewModel.activeSectionId()},
@@ -280,6 +282,9 @@ export class RegionFocusSwitcher extends Disposable {
280282
if (comesFromKeyboard) {
281283
panelElement?.setAttribute('tabindex', '-1');
282284
panelElement?.focus();
285+
if (activeElementIsInPanel) {
286+
this._prevFocusedElements[current.id] = null;
287+
}
283288
} else {
284289
this.reset();
285290
}

app/client/ui/PagePanels.ts

+1
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export function pagePanels(
309309
),
310310

311311
cssContentMainPane(
312+
testId('main-content'),
312313
regionFocusSwitcher?.panelAttrs('main', t('Main content')),
313314
page.contentMain,
314315
),

test/nbrowser/RegionFocusSwitcher.ts

+296
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { assert, driver, Key } from "mocha-webdriver";
2+
import { describe } from "mocha";
3+
import * as gu from "test/nbrowser/gristUtils";
4+
import { setupTestSuite } from "test/nbrowser/testUtils";
5+
6+
const isClipboardFocused = () => {
7+
return gu.hasFocus('textarea.copypaste.mousetrap');
8+
};
9+
10+
const isNormalElementFocused = async (containerSelector?: string) => {
11+
const activeElement = await gu.getActiveElement();
12+
const isException = await activeElement.matches(
13+
'.test-left-panel, .test-top-header, .test-right-panel, .test-main-content, body, textarea.copypaste.mousetrap'
14+
);
15+
const isInContainer = containerSelector
16+
? await activeElement.matches(`${containerSelector} *`)
17+
: true;
18+
return !isException && isInContainer;
19+
};
20+
21+
/**
22+
* tab twice: if we managed to focus things we consider "normal elements", we assume we can use tab to navigate
23+
*/
24+
const assertTabToNavigate = async (containerSelector?: string) => {
25+
await driver.sendKeys(Key.TAB);
26+
assert.isTrue(await isNormalElementFocused(containerSelector));
27+
28+
await driver.sendKeys(Key.TAB);
29+
assert.isTrue(await isNormalElementFocused(containerSelector));
30+
};
31+
32+
const cycle = async (dir: 'forward' | 'backward' = 'forward') => {
33+
const modKey = await gu.modKey();
34+
const shortcut = dir === 'forward'
35+
? Key.chord(modKey, 'o')
36+
: Key.chord(modKey, Key.SHIFT, 'O');
37+
38+
await gu.sendKeys(shortcut);
39+
};
40+
41+
const toggleCreatorPanelFocus = async () => {
42+
const modKey = await gu.modKey();
43+
await gu.sendKeys(Key.chord(modKey, Key.ALT, 'o'));
44+
};
45+
46+
const panelMatchs = {
47+
left: '.test-left-panel',
48+
top: '.test-top-header',
49+
right: '.test-right-panel',
50+
main: '.test-main-content',
51+
};
52+
const assertPanelFocus = async (panel: 'left' | 'top' | 'right' | 'main', expected: boolean = true) => {
53+
assert.equal(await gu.hasFocus(panelMatchs[panel]), expected);
54+
};
55+
56+
const assertSectionFocus = async (sectionId: number, expected: boolean = true) => {
57+
assert.equal(await isClipboardFocused(), expected);
58+
assert.equal(await gu.getSectionId() === sectionId, expected);
59+
};
60+
61+
/**
62+
* check if we can do a full cycle through regions with nextRegion/prevRegion commands
63+
*
64+
* `sections` is the number of view sections currently on the page.
65+
*/
66+
const assertCycleThroughRegions = async ({ sections = 1 }: { sections?: number } = {}) => {
67+
await cycle();
68+
await assertPanelFocus('left');
69+
70+
await cycle();
71+
await assertPanelFocus('top');
72+
73+
if (sections) {
74+
let sectionsCount = 0;
75+
while (sectionsCount < sections) {
76+
await cycle();
77+
assert.isTrue(await isClipboardFocused());
78+
sectionsCount++;
79+
}
80+
} else {
81+
await cycle();
82+
await assertPanelFocus('main');
83+
}
84+
85+
await cycle();
86+
await assertPanelFocus('left');
87+
88+
if (sections) {
89+
let sectionsCount = 0;
90+
while (sectionsCount < sections) {
91+
await cycle('backward');
92+
assert.isTrue(await isClipboardFocused());
93+
sectionsCount++;
94+
}
95+
} else {
96+
await cycle('backward');
97+
await assertPanelFocus('main');
98+
}
99+
100+
await cycle('backward');
101+
await assertPanelFocus('top');
102+
103+
await cycle('backward');
104+
await assertPanelFocus('left');
105+
};
106+
107+
describe("RegionFocusSwitcher", function () {
108+
this.timeout(60000);
109+
const cleanup = setupTestSuite();
110+
111+
it("should tab though elements in non-document pages", async () => {
112+
const session = await gu.session().teamSite.login();
113+
114+
await session.loadDocMenu("/");
115+
await assertTabToNavigate();
116+
117+
await gu.openProfileSettingsPage();
118+
await assertTabToNavigate();
119+
});
120+
121+
it("should keep the active section focused at document page load", async () => {
122+
const session = await gu.session().teamSite.login();
123+
await session.tempDoc(cleanup, 'Hello.grist');
124+
125+
assert.isTrue(await isClipboardFocused());
126+
assert.equal(await gu.getActiveCell().getText(), 'hello');
127+
await driver.sendKeys(Key.TAB);
128+
// after pressing tab once, we should be on the [first row, second column]-cell
129+
const secondCellText = await gu.getCell(1, 1).getText();
130+
const activeCellText = await gu.getActiveCell().getText();
131+
assert.equal(activeCellText, secondCellText);
132+
assert.isTrue(await isClipboardFocused());
133+
});
134+
135+
it("should cycle through regions with (Shift+)Ctrl+O", async () => {
136+
const session = await gu.session().teamSite.login();
137+
await session.loadDocMenu("/");
138+
139+
await assertCycleThroughRegions({ sections: 0 });
140+
141+
await session.tempNewDoc(cleanup);
142+
await assertCycleThroughRegions({ sections: 1 });
143+
144+
await gu.addNewSection(/Card List/, /Table1/);
145+
await gu.reloadDoc();
146+
await assertCycleThroughRegions({ sections: 2 });
147+
});
148+
149+
it("should toggle creator panel with Alt+Ctrl+O", async () => {
150+
const session = await gu.session().teamSite.login();
151+
await session.tempNewDoc(cleanup);
152+
153+
const firstSectionId = await gu.getSectionId();
154+
155+
// test if shortcut works with one view section:
156+
// press the shortcut two times to focus creator panel, then focus back the view section
157+
await toggleCreatorPanelFocus();
158+
await assertPanelFocus('right');
159+
160+
await toggleCreatorPanelFocus();
161+
await assertSectionFocus(firstSectionId);
162+
163+
// add a new section, make sure it's the active section/focus after creation
164+
await gu.addNewSection(/Card List/, /Table1/);
165+
const secondSectionId = await gu.getSectionId();
166+
await assertSectionFocus(secondSectionId);
167+
168+
// toggle creator panel again: make sure it goes back to the new section
169+
await toggleCreatorPanelFocus();
170+
await assertPanelFocus('right');
171+
172+
await toggleCreatorPanelFocus();
173+
await assertSectionFocus(secondSectionId);
174+
175+
// combine with cycle shortcut: when focus is on a panel, toggling creator panel focuses back the current view
176+
await cycle();
177+
await assertPanelFocus('left');
178+
179+
await toggleCreatorPanelFocus();
180+
await assertPanelFocus('right');
181+
182+
await toggleCreatorPanelFocus();
183+
await assertSectionFocus(secondSectionId);
184+
185+
// cycle to previous section and make sure all focus is good
186+
await cycle('backward');
187+
await assertSectionFocus(firstSectionId);
188+
189+
await toggleCreatorPanelFocus();
190+
await assertPanelFocus('right');
191+
192+
await toggleCreatorPanelFocus();
193+
await assertSectionFocus(firstSectionId);
194+
195+
await toggleCreatorPanelFocus();
196+
await assertPanelFocus('right');
197+
198+
await cycle();
199+
await assertSectionFocus(secondSectionId);
200+
201+
await toggleCreatorPanelFocus();
202+
await toggleCreatorPanelFocus();
203+
await assertSectionFocus(secondSectionId);
204+
});
205+
206+
it("should tab through elements when inside a region", async function() {
207+
const session = await gu.session().teamSite.login();
208+
await session.tempNewDoc(cleanup);
209+
210+
await cycle();
211+
await assertTabToNavigate('.test-left-panel');
212+
213+
await cycle();
214+
await assertTabToNavigate('.test-top-header');
215+
216+
await toggleCreatorPanelFocus();
217+
await assertTabToNavigate('.test-right-panel');
218+
219+
await toggleCreatorPanelFocus();
220+
await driver.sendKeys(Key.TAB);
221+
assert.isTrue(await isClipboardFocused());
222+
});
223+
224+
it("should exit from a region when pressing Esc", async function() {
225+
const session = await gu.session().teamSite.login();
226+
await session.tempNewDoc(cleanup);
227+
228+
await cycle();
229+
await driver.sendKeys(Key.ESCAPE);
230+
await assertPanelFocus('left', false);
231+
assert.isTrue(await isClipboardFocused());
232+
});
233+
234+
it("should remember the last focused element in a panel", async function() {
235+
const session = await gu.session().teamSite.login();
236+
await session.tempNewDoc(cleanup);
237+
238+
await cycle();
239+
await driver.sendKeys(Key.TAB);
240+
assert.isTrue(await isNormalElementFocused('.test-left-panel'));
241+
242+
await cycle(); // top
243+
await cycle(); // main
244+
await cycle(); // back to left
245+
assert.isTrue(await isNormalElementFocused('.test-left-panel'));
246+
247+
// when pressing escape in that case, first focus back to the panel…
248+
await driver.sendKeys(Key.ESCAPE);
249+
await assertPanelFocus('left');
250+
251+
// … then reset the kb focus as usual
252+
await driver.sendKeys(Key.ESCAPE);
253+
assert.isTrue(await isClipboardFocused());
254+
});
255+
256+
it("should focus a panel-region when clicking an input child element", async function() {
257+
const session = await gu.session().teamSite.login();
258+
await session.tempNewDoc(cleanup);
259+
260+
// Click on an input on the top panel
261+
await driver.find('.test-bc-doc').click();
262+
await driver.sendKeys(Key.TAB);
263+
assert.isTrue(await isNormalElementFocused('.test-top-header'));
264+
265+
// in that case (mouse click) when pressing esc, we directly focus back to view section
266+
await driver.sendKeys(Key.ESCAPE);
267+
await assertPanelFocus('top', false);
268+
assert.isTrue(await isClipboardFocused());
269+
});
270+
271+
it("should focus a section-region when clicking on it", async function() {
272+
const session = await gu.session().teamSite.login();
273+
await session.tempNewDoc(cleanup);
274+
275+
await cycle(); // left
276+
await driver.sendKeys(Key.TAB);
277+
assert.isTrue(await isNormalElementFocused('.test-left-panel'));
278+
279+
await gu.getActiveCell().click();
280+
281+
await assertPanelFocus('left', false);
282+
assert.isTrue(await isClipboardFocused());
283+
});
284+
285+
it("should keep the active section focused when clicking a link or button of a panel-region", async function() {
286+
const session = await gu.session().teamSite.login();
287+
await session.tempNewDoc(cleanup);
288+
289+
await gu.enterCell('test');
290+
await driver.find('.test-undo').click();
291+
await assertPanelFocus('top', false);
292+
assert.isTrue(await isClipboardFocused());
293+
});
294+
295+
afterEach(() => gu.checkForErrors());
296+
});

test/nbrowser/gristUtils.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ export async function selectAll() {
255255
await driver.executeScript('document.activeElement.select()');
256256
}
257257

258+
export async function getActiveElement() {
259+
return await driver.executeScript<WebElement>('return document.activeElement');
260+
}
261+
258262
/**
259263
* Returns a WebElementPromise for the .viewsection_content element for the section which contains
260264
* the given text (case insensitive) content.
@@ -1162,6 +1166,10 @@ export async function docMenuImport(filePath: string) {
11621166
});
11631167
}
11641168

1169+
export async function hasFocus(selector: string): Promise<boolean> {
1170+
return await driver.find(selector).hasFocus();
1171+
}
1172+
11651173
/**
11661174
* Wait for the focus to return to the main application, i.e. the special .copypaste element that
11671175
* normally has it (as opposed to an open cell editor, or a focus in some input or menu). Specify
@@ -1175,7 +1183,7 @@ export async function waitAppFocus(yesNo: boolean = true): Promise<void> {
11751183
* Wait for the focus to be on the first element matching given selector.
11761184
*/
11771185
export async function waitForFocus(selector: string): Promise<void> {
1178-
await driver.wait(async () => (await driver.find(selector).hasFocus()), 1000);
1186+
await driver.wait(async () => (await hasFocus(selector)), 1000);
11791187
}
11801188

11811189
export async function waitForLabelInput(): Promise<void> {

0 commit comments

Comments
 (0)