From bab1763574c29e5b4e6f9fb217f7358e251988a4 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 20 Jul 2025 12:45:41 +0200 Subject: [PATCH 01/10] Fine tune the custom elements manifest --- src/vscode-tree/vscode-tree.ts | 36 ++++++++++------------------------ 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/vscode-tree/vscode-tree.ts b/src/vscode-tree/vscode-tree.ts index 733650640..843a7fb23 100644 --- a/src/vscode-tree/vscode-tree.ts +++ b/src/vscode-tree/vscode-tree.ts @@ -59,11 +59,6 @@ const listenedKeys: ListenedKey[] = [ 'Escape', 'Shift', ]; -const DEFAULT_HIDE_ARROWS = false; -const DEFAULT_INDENT = 8; -const DEFAULT_MULTI_SELECT = false; -const DEFAULT_EXPAND_MODE = ExpandMode.singleClick; -const DEFAULT_INDENT_GUIDE_DISPLAY = IndentGuides.onHover; /** * @tag vscode-tree @@ -78,12 +73,9 @@ export class VscodeTree extends VscElement { * Although arrows are always visible in the Tree component by default in VSCode, some icon sets * (e.g., Material Icon Theme) allow disabling them in the file explorer view. This flag makes it * possible to mimic that behavior. - * - * @type {boolean} - * @default false */ @property({type: Boolean, reflect: true, attribute: 'hide-arrows'}) - hideArrows: boolean = DEFAULT_HIDE_ARROWS; + hideArrows: boolean = false; /** * Controls how tree folders are expanded when clicked. This property is designed to use @@ -98,20 +90,16 @@ export class VscodeTree extends VscElement { * ``` * * @type {'singleClick' | 'doubleClick'} - * @default 'singleClick' */ @property({type: String, attribute: 'expand-mode'}) - expandMode: ExpandMode = DEFAULT_EXPAND_MODE; + expandMode: ExpandMode = 'singleClick'; /** * Controls the indentation in pixels. This property is designed to use the * `workbench.tree.indent` setting. - * - * @type {number} - * @default 8 */ @property({type: Number, reflect: true}) - indent: number = DEFAULT_INDENT; + indent: number = 8; /** * Controls whether the tree should render indent guides. This property is @@ -126,7 +114,6 @@ export class VscodeTree extends VscElement { * ``` * * @type {'none' | 'onHover' | 'always'} - * @default 'onHover' */ @property({ type: String, @@ -134,16 +121,13 @@ export class VscodeTree extends VscElement { useDefault: true, reflect: true, }) - indentGuides: IndentGuideDisplay = DEFAULT_INDENT_GUIDE_DISPLAY; + indentGuides: IndentGuideDisplay = 'onHover'; /** * Allows selecting multiple items. - * - * @type {boolean} - * @default false */ @property({type: Boolean, reflect: true, attribute: 'multi-select'}) - multiSelect: boolean = DEFAULT_MULTI_SELECT; + multiSelect: boolean = false; //#endregion @@ -171,11 +155,11 @@ export class VscodeTree extends VscElement { @provide({context: configContext}) private _configContext: ConfigContext = { - hideArrows: DEFAULT_HIDE_ARROWS, - expandMode: DEFAULT_EXPAND_MODE, - indent: DEFAULT_INDENT, - indentGuides: DEFAULT_INDENT_GUIDE_DISPLAY, - multiSelect: DEFAULT_MULTI_SELECT, + hideArrows: this.hideArrows, + expandMode: this.expandMode, + indent: this.indent, + indentGuides: this.indentGuides, + multiSelect: this.multiSelect, }; @queryAssignedElements({selector: 'vscode-tree-item'}) From ae259fae24559cdb2022b7a35652a7da0f54fe4f Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 20 Jul 2025 14:49:14 +0200 Subject: [PATCH 02/10] Sort properties alphabetically --- src/vscode-tree/vscode-tree.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vscode-tree/vscode-tree.ts b/src/vscode-tree/vscode-tree.ts index 843a7fb23..717b90ef0 100644 --- a/src/vscode-tree/vscode-tree.ts +++ b/src/vscode-tree/vscode-tree.ts @@ -69,14 +69,6 @@ export class VscodeTree extends VscElement { //#region properties - /** - * Although arrows are always visible in the Tree component by default in VSCode, some icon sets - * (e.g., Material Icon Theme) allow disabling them in the file explorer view. This flag makes it - * possible to mimic that behavior. - */ - @property({type: Boolean, reflect: true, attribute: 'hide-arrows'}) - hideArrows: boolean = false; - /** * Controls how tree folders are expanded when clicked. This property is designed to use * the `workbench.tree.expandMode` setting. @@ -94,6 +86,14 @@ export class VscodeTree extends VscElement { @property({type: String, attribute: 'expand-mode'}) expandMode: ExpandMode = 'singleClick'; + /** + * Although arrows are always visible in the Tree component by default in VSCode, some icon sets + * (e.g., Material Icon Theme) allow disabling them in the file explorer view. This flag makes it + * possible to mimic that behavior. + */ + @property({type: Boolean, reflect: true, attribute: 'hide-arrows'}) + hideArrows: boolean = false; + /** * Controls the indentation in pixels. This property is designed to use the * `workbench.tree.indent` setting. From cea137382f3462506b0702ce142fa8301ca55439 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 20 Jul 2025 14:49:27 +0200 Subject: [PATCH 03/10] Add tests --- src/vscode-tree/vscode-tree.test.ts | 288 ++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) diff --git a/src/vscode-tree/vscode-tree.test.ts b/src/vscode-tree/vscode-tree.test.ts index f635504a0..bcae80e17 100644 --- a/src/vscode-tree/vscode-tree.test.ts +++ b/src/vscode-tree/vscode-tree.test.ts @@ -10,6 +10,294 @@ describe('vscode-tree', () => { expect(el).to.instanceOf(VscodeTree); }); + it('is accessible', async () => { + const el = await fixture(html` + + + Item 1 + + Item 1.1 + Item 1.1.1 + + + + Item 2 + + Item 2.1 + Item 2.1.1 + + + + Item 3 + + Item 3.1 + Item 3.1.1 + + + + `); + + expect(el).to.be.accessible; + }); + + describe('default values', () => { + it('expandMode', () => { + const el = document.createElement('vscode-tree'); + expect(el.expandMode).to.eq('singleClick'); + }); + + it('hideArrows', () => { + const el = document.createElement('vscode-tree'); + expect(el.hideArrows).to.be.false; + }); + + it('indent', () => { + const el = document.createElement('vscode-tree'); + expect(el.indent).to.eq(8); + }); + + it('indentGuides', () => { + const el = document.createElement('vscode-tree'); + expect(el.indentGuides).to.eq('onHover'); + }); + }); + + describe('public methods', () => { + it('expands all', async () => { + const el = await fixture(html` + + + Item 1 + + Item 1.1 + Item 1.1.1 + + + + Item 2 + + Item 2.1 + Item 2.1.1 + + + + Item 3 + + Item 3.1 + Item 3.1.1 + + + + `); + + el.expandAll(); + await el.updateComplete; + + expect(el).lightDom.to.eq( + ` + + Item 1 + + Item 1.1 + Item 1.1.1 + + + + Item 2 + + Item 2.1 + Item 2.1.1 + + + + Item 3 + + Item 3.1 + Item 3.1.1 + + + `, + { + ignoreAttributes: [ + 'aria-disabled', + 'data-path', + 'level', + 'role', + 'slot', + 'tabindex', + ], + } + ); + }); + + it('closes all', async () => { + const el = await fixture(html` + + + Item 1 + + Item 1.1 + Item 1.1.1 + + + + Item 2 + + Item 2.1 + Item 2.1.1 + + + + Item 3 + + Item 3.1 + Item 3.1.1 + + + + `); + + el.collapseAll(); + await el.updateComplete; + + expect(el).lightDom.to.eq( + ` + + + + `, + { + ignoreAttributes: [ + 'aria-disabled', + 'data-path', + 'level', + 'role', + 'slot', + 'tabindex', + ], + } + ); + }); + }); + + describe('expand mode', () => { + it('singleClick', async () => { + const el = await fixture(html` + + + Item 1 + + Item 1.1 + Item 1.1.1 + + + + `); + const branch = el.querySelector('vscode-tree-item')!; + + branch.shadowRoot + ?.querySelector('.wrapper') + ?.dispatchEvent(new PointerEvent('click')); + + expect(branch.open).to.be.true; + }); + + it('doubleClick', async () => { + const el = await fixture(html` + + + Item 1 + + Item 1.1 + Item 1.1.1 + + + + `); + const branch = el.querySelector('vscode-tree-item')!; + + branch.shadowRoot + ?.querySelector('.wrapper') + ?.dispatchEvent(new PointerEvent('dblclick')); + + expect(branch.open).to.be.true; + }); + }); + + describe('indent guides', () => { + it('indentGuides onHover', async () => { + const el = await fixture(html` + + + Item 1 + Item 1.1 + Item 1.2 + + + `); + const branch = el.querySelector('vscode-tree-item')!; + const childrenWrapper = + branch.shadowRoot!.querySelector('.children')!; + + expect(childrenWrapper.classList.contains('guide')).to.be.true; + expect(childrenWrapper.classList.contains('default-guide')).to.be.true; + }); + + it('indentGuides always', async () => { + const el = await fixture(html` + + + Item 1 + Item 1.1 + Item 1.2 + + + `); + const branch = el.querySelector('vscode-tree-item')!; + const childrenWrapper = + branch.shadowRoot!.querySelector('.children')!; + + expect(childrenWrapper.classList.contains('guide')).to.be.true; + expect(childrenWrapper.classList.contains('default-guide')).to.be.true; + }); + + it('indentGuides none', async () => { + const el = await fixture(html` + + + Item 1 + Item 1.1 + Item 1.2 + + + `); + const branch = el.querySelector('vscode-tree-item')!; + const childrenWrapper = + branch.shadowRoot!.querySelector('.children')!; + + expect(childrenWrapper.classList.contains('guide')).to.be.false; + expect(childrenWrapper.classList.contains('default-guide')).to.be.false; + }); + }); + it('focuses first item by default', async () => { const el = await fixture(html` From c1f0d0b56772236de22a748cdc246cddaefca739 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 20 Jul 2025 15:24:57 +0200 Subject: [PATCH 04/10] Add tests --- src/vscode-tree/vscode-tree.test.ts | 72 ++++++++++++++++------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/src/vscode-tree/vscode-tree.test.ts b/src/vscode-tree/vscode-tree.test.ts index bcae80e17..26ca0d901 100644 --- a/src/vscode-tree/vscode-tree.test.ts +++ b/src/vscode-tree/vscode-tree.test.ts @@ -40,6 +40,42 @@ describe('vscode-tree', () => { expect(el).to.be.accessible; }); + it('focuses first item by default', async () => { + const el = await fixture(html` + + Item 1 + Item 2 + + `); + const firstItem = + el.querySelectorAll('vscode-tree-item')[0]!; + const secondItem = + el.querySelectorAll('vscode-tree-item')[1]!; + + expect(firstItem.tabIndex).to.eq(0); + expect(firstItem.active).to.be.true; + expect(secondItem.tabIndex).to.eq(-1); + expect(secondItem.active).to.be.false; + }); + + it('focuses active item on load', async () => { + const el = await fixture(html` + + Item 1 + Item 2 + + `); + const firstItem = + el.querySelectorAll('vscode-tree-item')[0]!; + const secondItem = + el.querySelectorAll('vscode-tree-item')[1]!; + + expect(firstItem.tabIndex).to.eq(-1); + expect(firstItem.active).to.be.false; + expect(secondItem.tabIndex).to.eq(0); + expect(secondItem.active).to.be.true; + }); + describe('default values', () => { it('expandMode', () => { const el = document.createElement('vscode-tree'); @@ -298,40 +334,10 @@ describe('vscode-tree', () => { }); }); - it('focuses first item by default', async () => { - const el = await fixture(html` - - Item 1 - Item 2 - - `); - const firstItem = - el.querySelectorAll('vscode-tree-item')[0]!; - const secondItem = - el.querySelectorAll('vscode-tree-item')[1]!; - - expect(firstItem.tabIndex).to.eq(0); - expect(firstItem.active).to.be.true; - expect(secondItem.tabIndex).to.eq(-1); - expect(secondItem.active).to.be.false; - }); - - it('focuses active item on load', async () => { - const el = await fixture(html` - - Item 1 - Item 2 - - `); - const firstItem = - el.querySelectorAll('vscode-tree-item')[0]!; - const secondItem = - el.querySelectorAll('vscode-tree-item')[1]!; + describe('hide arrows', () => { + it('hide arrows'); - expect(firstItem.tabIndex).to.eq(-1); - expect(firstItem.active).to.be.false; - expect(secondItem.tabIndex).to.eq(0); - expect(secondItem.active).to.be.true; + it('show arrows'); }); it('focuses next item when arrow down key is pressed', async () => { From 673a1b0c6517330e3b2aaf2f8d12f8fef9746727 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 20 Jul 2025 23:38:51 +0200 Subject: [PATCH 05/10] Fix css --- src/vscode-tree-item/vscode-tree-item.styles.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vscode-tree-item/vscode-tree-item.styles.ts b/src/vscode-tree-item/vscode-tree-item.styles.ts index e6330a67b..a22586807 100644 --- a/src/vscode-tree-item/vscode-tree-item.styles.ts +++ b/src/vscode-tree-item/vscode-tree-item.styles.ts @@ -18,6 +18,10 @@ const styles: CSSResultGroup = [ user-select: none; } + ::slotted(vscode-icon) { + display: block; + } + .root { display: block; } @@ -85,7 +89,11 @@ const styles: CSSResultGroup = [ .icon-container { align-items: center; display: flex; - height: 22px; + height: 16px; + margin-bottom: 3px; + margin-top: 3px; + overflow: hidden; + width: 16px; } .icon-container slot { From 6bf7b20ba7eb3c7e1b59f7e88d85035b9175d9f4 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 24 Jul 2025 18:00:23 +0200 Subject: [PATCH 06/10] Fix css --- src/vscode-tree-item/vscode-tree-item.styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vscode-tree-item/vscode-tree-item.styles.ts b/src/vscode-tree-item/vscode-tree-item.styles.ts index a22586807..d315185bc 100644 --- a/src/vscode-tree-item/vscode-tree-item.styles.ts +++ b/src/vscode-tree-item/vscode-tree-item.styles.ts @@ -89,11 +89,9 @@ const styles: CSSResultGroup = [ .icon-container { align-items: center; display: flex; - height: 16px; margin-bottom: 3px; margin-top: 3px; overflow: hidden; - width: 16px; } .icon-container slot { @@ -101,7 +99,9 @@ const styles: CSSResultGroup = [ } .icon-container.has-icon { + height: 16px; margin-right: 6px; + width: 16px; } .children { From 4e0b89c0e9343ca52868a268453f71aeba5cc904 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 24 Jul 2025 21:47:29 +0200 Subject: [PATCH 07/10] Add tests --- dev/vscode-tree/multi-select.html | 2 + src/vscode-tree/vscode-tree.test.ts | 82 +++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/dev/vscode-tree/multi-select.html b/dev/vscode-tree/multi-select.html index a9883dd7d..6e458d707 100644 --- a/dev/vscode-tree/multi-select.html +++ b/dev/vscode-tree/multi-select.html @@ -237,6 +237,8 @@

Basic example

li.focus(); }); }); + + logEvents('vscode-tree', 'vsc-tree-select'); diff --git a/src/vscode-tree/vscode-tree.test.ts b/src/vscode-tree/vscode-tree.test.ts index 26ca0d901..4de4c8d85 100644 --- a/src/vscode-tree/vscode-tree.test.ts +++ b/src/vscode-tree/vscode-tree.test.ts @@ -3,6 +3,8 @@ import {sendKeys} from '@web/test-runner-commands'; import '../vscode-tree-item/vscode-tree-item.js'; import {VscodeTreeItem} from '../vscode-tree-item/vscode-tree-item.js'; import {VscodeTree} from './index.js'; +import sinon from 'sinon'; +import { clickOnElement } from '../includes/test-helpers.js'; describe('vscode-tree', () => { it('is defined', () => { @@ -335,9 +337,37 @@ describe('vscode-tree', () => { }); describe('hide arrows', () => { - it('hide arrows'); + it('show arrows', async () => { + const el = await fixture( + html` + + Item 1 + Item 1.1 + Item 1.2 + + ` + ); + + const branch = el.querySelector('vscode-tree-item[open]')!; - it('show arrows'); + expect(branch.shadowRoot?.querySelector('.arrow-container')).to.be.ok; + }); + + it('hide arrows', async () => { + const el = await fixture( + html` + + Item 1 + Item 1.1 + Item 1.2 + + ` + ); + + const branch = el.querySelector('vscode-tree-item[open]')!; + + expect(branch.shadowRoot?.querySelector('.arrow-container')).to.be.null; + }); }); it('focuses next item when arrow down key is pressed', async () => { @@ -360,15 +390,57 @@ describe('vscode-tree', () => { expect(items[1].active).to.be.true; }); - it('selects item with Enter key press'); - it('selects item with click on it'); + it('selects item with Enter key press', async () => { + const el = await fixture( + html` + Item 1 + Item 2 + ` + ); + const spy = sinon.spy(); + + el.addEventListener('vsc-tree-select', spy); + + const firstChild = el.querySelector('vscode-tree-item')!; + firstChild.active = true; + firstChild.focus(); + await sendKeys({down: 'Enter'}); + + expect(firstChild.selected).to.be.true; + expect(spy.getCalls()[0].args[0].type).to.eq('vsc-tree-select'); + }); + + it('selects item with click on it', async () => { + const el = await fixture( + html` + Item 1 + Item 2 + ` + ); + const spy = sinon.spy(); + + el.addEventListener('vsc-tree-select', spy); + + const firstChild = el.querySelector('vscode-tree-item')!; + await clickOnElement(firstChild); + + expect(firstChild.selected).to.be.true; + expect(spy.getCalls()[0].args[0].type).to.eq('vsc-tree-select'); + }); + it('opens and selects branch item with Enter key press'); it('opens and selects branch item with click on it'); + it('selecting multiple items upwards with the mouse and the Shift key'); + it( 'expands selection of multiple items upwards with the mouse and the Shift key' ); - it('selecting multiple items downwards with the mouse and the Shift key'); + + it('selecting multiple items downwards with the mouse and the Shift key', async () => { + + }); + it( 'expands selection of multiple items downwards with the mouse and the Shift key' ); From b091c368d6b8cf8fcc57e51964d5b677a9149286 Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 26 Jul 2025 01:02:12 +0200 Subject: [PATCH 08/10] Add tests --- src/includes/test-helpers.ts | 61 +++++++ src/vscode-tree/vscode-tree.test.ts | 269 ++++++++++++++++++++++++++-- 2 files changed, 316 insertions(+), 14 deletions(-) diff --git a/src/includes/test-helpers.ts b/src/includes/test-helpers.ts index 4de7beb26..6bb4e8c16 100644 --- a/src/includes/test-helpers.ts +++ b/src/includes/test-helpers.ts @@ -108,3 +108,64 @@ export async function dragElement( await sendMouse({type: 'up'}); } + +type AllTagNames = keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap; + +type TagNameToElement = + K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : + K extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[K] : + Element; + +export function $(selector: K): TagNameToElement; +export function $(root: Element, selector: K): TagNameToElement; +export function $(selector: string): T; +export function $(root: Element, selector: string): T; +export function $( + arg1: string | Element, + arg2?: string +): T { + let result: Element | null; + + if (typeof arg1 === 'string') { + result = document.querySelector(arg1); + } else if (arg1 instanceof Element && typeof arg2 === 'string') { + result = arg1.querySelector(arg2); + } else { + throw new Error('Invalid arguments passed to $()'); + } + + if (!result) { + const selector = typeof arg1 === 'string' ? arg1 : arg2!; + const context = typeof arg1 === 'string' ? 'document' : 'root element'; + throw new Error(`No match for selector: ${selector} in ${context}`); + } + + return result as T; +} + +export function $$(selector: K): NodeListOf>; +export function $$(root: Element, selector: K): NodeListOf>; +export function $$(selector: string): NodeListOf; +export function $$(root: Element, selector: string): NodeListOf; +export function $$( + arg1: string | Element, + arg2?: string +): NodeListOf { + let result: NodeListOf; + + if (typeof arg1 === 'string') { + result = document.querySelectorAll(arg1); + } else if (arg1 instanceof Element && typeof arg2 === 'string') { + result = arg1.querySelectorAll(arg2); + } else { + throw new Error('Invalid arguments passed to $$()'); + } + + if (result.length === 0) { + const selector = typeof arg1 === 'string' ? arg1 : arg2!; + const context = typeof arg1 === 'string' ? 'document' : 'root element'; + throw new Error(`No matches for selector: ${selector} in ${context}`); + } + + return result as NodeListOf; +} diff --git a/src/vscode-tree/vscode-tree.test.ts b/src/vscode-tree/vscode-tree.test.ts index 4de4c8d85..4706f49fd 100644 --- a/src/vscode-tree/vscode-tree.test.ts +++ b/src/vscode-tree/vscode-tree.test.ts @@ -1,10 +1,12 @@ import {expect, fixture, html} from '@open-wc/testing'; import {sendKeys} from '@web/test-runner-commands'; +import sinon from 'sinon'; +import {$$, clickOnElement} from '../includes/test-helpers.js'; +import {$} from '../includes/test-helpers.js'; + import '../vscode-tree-item/vscode-tree-item.js'; import {VscodeTreeItem} from '../vscode-tree-item/vscode-tree-item.js'; import {VscodeTree} from './index.js'; -import sinon from 'sinon'; -import { clickOnElement } from '../includes/test-helpers.js'; describe('vscode-tree', () => { it('is defined', () => { @@ -390,6 +392,26 @@ describe('vscode-tree', () => { expect(items[1].active).to.be.true; }); + it('focuses previous item when arrow up key is pressed', async () => { + const el = await fixture(html` + + Item 1 + Item 2 + + `); + + const items = $$('vscode-tree-item'); + + items[1].focus(); + await sendKeys({press: 'ArrowUp'}); + await el.updateComplete; + + expect(items[0].tabIndex).to.eq(0); + expect(items[0].active).to.be.true; + expect(items[1].tabIndex).to.eq(-1); + expect(items[1].active).to.be.false; + }); + it('selects item with Enter key press', async () => { const el = await fixture( html` @@ -401,7 +423,7 @@ describe('vscode-tree', () => { el.addEventListener('vsc-tree-select', spy); - const firstChild = el.querySelector('vscode-tree-item')!; + const firstChild = $(el, 'vscode-tree-item')!; firstChild.active = true; firstChild.focus(); await sendKeys({down: 'Enter'}); @@ -412,7 +434,7 @@ describe('vscode-tree', () => { it('selects item with click on it', async () => { const el = await fixture( - html` + html` Item 1 Item 2 ` @@ -428,20 +450,239 @@ describe('vscode-tree', () => { expect(spy.getCalls()[0].args[0].type).to.eq('vsc-tree-select'); }); - it('opens and selects branch item with Enter key press'); - it('opens and selects branch item with click on it'); + it('opens and selects branch item with Enter key press', async () => { + await fixture( + html` + Item 1 + + Item 2 + Item 2.1 + + ` + ); + + $('vscode-tree-item').focus(); + await sendKeys({down: 'ArrowDown'}); + await sendKeys({down: 'Enter'}); + + expect($('#item2').open).to.be.true; + expect($('#item2').selected).to.be.true; + }); + + it('opens and selects branch item with click on it', async () => { + await fixture( + html` + Item 1 + + Item 2 + Item 2.1 + + ` + ); + + await clickOnElement($('#item2')); + + expect($('#item2').open).to.be.true; + expect($('#item2').selected).to.be.true; + }); + + it('selecting multiple items upwards with the mouse and the Shift key', async () => { + await fixture( + html` + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + ` + ); - it('selecting multiple items upwards with the mouse and the Shift key'); + await clickOnElement($('#item5')); - it( - 'expands selection of multiple items upwards with the mouse and the Shift key' - ); + await sendKeys({down: 'Shift'}); + await clickOnElement($('#item1')); + await sendKeys({up: 'Shift'}); + + for (let i = 1; i <= 5; i++) { + expect($(`#item${i}`).selected).to.be.true; + } + }); + + it('expands selection of multiple items upwards with the mouse and the Shift key', async () => { + await fixture( + html` + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + ` + ); + + await clickOnElement($('#item5')); + + await sendKeys({down: 'Shift'}); + await clickOnElement($('#item3')); + await clickOnElement($('#item1')); + await sendKeys({up: 'Shift'}); + + for (let i = 1; i <= 5; i++) { + expect($(`#item${i}`).selected).to.be.true; + } + }); it('selecting multiple items downwards with the mouse and the Shift key', async () => { - + await fixture( + html` + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + ` + ); + + await clickOnElement($('#item1')); + + await sendKeys({down: 'Shift'}); + await clickOnElement($('#item5')); + await sendKeys({up: 'Shift'}); + + for (let i = 1; i <= 5; i++) { + expect($(`#item${i}`).selected).to.be.true; + } + }); + + it('expands selection of multiple items downwards with the mouse and the Shift key', async () => { + await fixture( + html` + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + ` + ); + + await clickOnElement($('#item1')); + + await sendKeys({down: 'Shift'}); + await clickOnElement($('#item3')); + await clickOnElement($('#item5')); + await sendKeys({up: 'Shift'}); + + for (let i = 1; i <= 5; i++) { + expect($(`#item${i}`).selected).to.be.true; + } + }); + + it('selecting multiple items with the mouse and the Ctrl key', async () => { + await fixture( + html` + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + ` + ); + + await clickOnElement($('#item1')); + await sendKeys({down: 'Control'}); + await clickOnElement($('#item3')); + await clickOnElement($('#item5')); + await sendKeys({up: 'Control'}); + + expect($('#item1').selected).to.be.true; + expect($('#item2').selected).to.be.false; + expect($('#item3').selected).to.be.true; + expect($('#item4').selected).to.be.false; + expect($('#item5').selected).to.be.true; + }); + + it('opens closed branch if the ArrowRight key is pressed', async () => { + await fixture( + html` + + Item 1 + Item 1.1 + + ` + ); + + $('vscode-tree-item').focus(); + await sendKeys({down: 'ArrowRight'}); + + expect($('vscode-tree-item').open).to.be.true; + }); + + it('select the first child when the ArrowRight key is pressed on an opened branch', async () => { + await fixture( + html` + + Item 1 + Item 1.1 + + ` + ); + + $('vscode-tree-item').active = true; + $('vscode-tree-item').focus(); + await sendKeys({down: 'ArrowRight'}); + + expect($('#item1_1').active).to.be.true; + }); + + it('closes the opened branch if the ArrowLeft key is pressed', async () => { + await fixture( + html` + + Item 1 + Item 1.1 + + ` + ); + + $('vscode-tree-item').focus(); + await sendKeys({down: 'ArrowLeft'}); + + expect($('vscode-tree-item').open).to.be.false; }); - it( - 'expands selection of multiple items downwards with the mouse and the Shift key' - ); + it('selects the parent branch if the ArrowLeft key is pressed on a leaf', async () => { + await fixture( + html` + + Item 1 + Item 1.1 + + ` + ); + + $('#item1_1').active = true; + $('#item1_1').focus(); + await sendKeys({down: 'ArrowLeft'}); + + expect($('vscode-tree-item').active).to.be.true; + }); + + it('selects the parent branch if the ArrowLeft key is pressed on a closed branch', async () => { + await fixture( + html` + + Item 1 + + Item 1.1 + Item 1.1.1 + + + ` + ); + + $('#item1_1').active = true; + $('#item1_1').focus(); + await sendKeys({down: 'ArrowLeft'}); + + expect($('vscode-tree-item').active).to.be.true; + }); }); From 04442cf7573efc1ce2531770faf5eb9c00854d3d Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 26 Jul 2025 01:23:07 +0200 Subject: [PATCH 09/10] Add tests --- src/vscode-tree/vscode-tree.test.ts | 63 ++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/vscode-tree/vscode-tree.test.ts b/src/vscode-tree/vscode-tree.test.ts index 4706f49fd..16fe0eba2 100644 --- a/src/vscode-tree/vscode-tree.test.ts +++ b/src/vscode-tree/vscode-tree.test.ts @@ -576,6 +576,42 @@ describe('vscode-tree', () => { } }); + it('toggle selection upwards with ArrowUp + Shift', async () => { + await fixture( + html` + Item 1 + Item 2 + ` + ); + + const items = $$('vscode-tree-item'); + items[1].active; + items[1].focus(); + await sendKeys({down: 'Shift'}); + await sendKeys({down: 'ArrowUp'}); + await sendKeys({up: 'Shift'}); + + expect($('vscode-tree-item').selected).to.be.false; + }); + + it('toggle selection downwards with ArrowDown + Shift', async () => { + await fixture( + html` + Item 1 + Item 2 + ` + ); + + const items = $$('vscode-tree-item'); + items[0].active; + items[0].focus(); + await sendKeys({down: 'Shift'}); + await sendKeys({down: 'ArrowDown'}); + await sendKeys({up: 'Shift'}); + + expect($$('vscode-tree-item')[1].selected).to.be.false; + }); + it('selecting multiple items with the mouse and the Ctrl key', async () => { await fixture( html` @@ -649,7 +685,7 @@ describe('vscode-tree', () => { expect($('vscode-tree-item').open).to.be.false; }); - it('selects the parent branch if the ArrowLeft key is pressed on a leaf', async () => { + it('pressing ArrowLeft on a leaf selects its parent branch', async () => { await fixture( html` @@ -666,7 +702,7 @@ describe('vscode-tree', () => { expect($('vscode-tree-item').active).to.be.true; }); - it('selects the parent branch if the ArrowLeft key is pressed on a closed branch', async () => { + it('pressing ArrowLeft on a collapsed branch selects its parent', async () => { await fixture( html` @@ -685,4 +721,27 @@ describe('vscode-tree', () => { expect($('vscode-tree-item').active).to.be.true; }); + + it('Ctrl + ArrowLeft collapses all branches', async () => { + await fixture( + html` + + Item 1 + + Item 1.1 + Item 1.1.1 + + + ` + ); + + $('vscode-tree-item').active = true; + $('vscode-tree-item').focus(); + await sendKeys({down: 'Control'}); + await sendKeys({down: 'ArrowLeft'}); + + $$('vscode-tree-item').forEach((i) => { + expect(i.open).to.be.false; + }); + }); }); From 888b6f404dcde07eed65d1c880eec25da90ec61a Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 26 Jul 2025 01:24:36 +0200 Subject: [PATCH 10/10] Prettier fix --- src/includes/test-helpers.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/includes/test-helpers.ts b/src/includes/test-helpers.ts index 6bb4e8c16..f1072bb55 100644 --- a/src/includes/test-helpers.ts +++ b/src/includes/test-helpers.ts @@ -112,14 +112,22 @@ export async function dragElement( type AllTagNames = keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap; type TagNameToElement = - K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : - K extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[K] : - Element; + K extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[K] + : K extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[K] + : Element; export function $(selector: K): TagNameToElement; -export function $(root: Element, selector: K): TagNameToElement; +export function $( + root: Element, + selector: K +): TagNameToElement; export function $(selector: string): T; -export function $(root: Element, selector: string): T; +export function $( + root: Element, + selector: string +): T; export function $( arg1: string | Element, arg2?: string @@ -143,10 +151,20 @@ export function $( return result as T; } -export function $$(selector: K): NodeListOf>; -export function $$(root: Element, selector: K): NodeListOf>; -export function $$(selector: string): NodeListOf; -export function $$(root: Element, selector: string): NodeListOf; +export function $$( + selector: K +): NodeListOf>; +export function $$( + root: Element, + selector: K +): NodeListOf>; +export function $$( + selector: string +): NodeListOf; +export function $$( + root: Element, + selector: string +): NodeListOf; export function $$( arg1: string | Element, arg2?: string