• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

adobe / spectrum-web-components / 18017563598

25 Sep 2025 06:48PM UTC coverage: 97.916% (-0.09%) from 98.002%
18017563598

push

github

web-flow
chore: update playwright and @web/test-runner dependencies with comprehensive test refactoring (#5578)

This pull request represents a comprehensive update to testing infrastructure and dependencies across the entire Spectrum Web Components codebase. The changes include major dependency updates for Playwright and @web/test-runner packages, extensive test refactoring for improved reliability and maintainability, and enhanced CI/CD configurations.

Major Dependency Updates
Playwright:

Updated Docker image in CI from mcr.microsoft.com/playwright:v1.44.0 to v1.53.1
Updated @playwright/test version from ^1.44.0 to ^1.53.1
@web/test-runner Ecosystem:

Updated @web/test-runner from ^0.18.3 to ^0.20.2 across multiple packages
Updated @web/test-runner-junit-reporter from ^0.7.2 to ^0.8.0
Updated @web/test-runner-playwright from ^0.11.0 to ^0.11.1
Updated @web/test-runner-visual-regression from ^0.9.0 to ^0.10.0
Updated wireit from ^0.14.3 to ^0.14.12
Infrastructure Improvements
CI/CD Configuration:

Enhanced CircleCI configuration with updated Docker images and improved parallelism
Added new Chromium memory testing configuration (web-test-runner.config.ci-chromium-memory.js)
Updated GitHub Actions workflows for better coverage reporting
Added concurrency settings across all browser test configurations
ESLint and Code Quality:

Expanded ESLint coverage to include **/stories/*.ts files
Updated ESM import syntax from assert { type: 'json' } to with { type: 'json' }
Fixed import paths for visual regression commands
Dependency Management:

Added comprehensive patching system documentation in CONTRIBUTING.md
Created Yarn patches for @web/test-runner-playwright and @web/test-runner-visual-regression
Removed legacy patch-package dependency in favor of Yarn 4's built-in patching
Comprehensive Test Suite Refactoring (132 files affected)
Testing Helper Functions:

Refactored mouse interaction helpers: sendMouse → mouseClickOn, mouseClickAway, mouseMoveOver, mouseMoveAwa... (continued)

5324 of 5638 branches covered (94.43%)

Branch coverage included in aggregate %.

91 of 93 new or added lines in 8 files covered. (97.85%)

204 existing lines in 31 files now uncovered.

34050 of 34574 relevant lines covered (98.48%)

628.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.6
/packages/split-view/src/SplitView.ts
1
/**
2✔
2
 * Copyright 2025 Adobe. All rights reserved.
2✔
3
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
2✔
4
 * you may not use this file except in compliance with the License. You may obtain a copy
2✔
5
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
2✔
6
 *
2✔
7
 * Unless required by applicable law or agreed to in writing, software distributed under
2✔
8
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
2✔
9
 * OF ANY KIND, either express or implied. See the License for the specific language
2✔
10
 * governing permissions and limitations under the License.
2✔
11
 */
2✔
12

2✔
13
import {
2✔
14
    CSSResultArray,
2✔
15
    html,
2✔
16
    LitElement,
2✔
17
    nothing,
2✔
18
    PropertyValues,
2✔
19
    SpectrumElement,
2✔
20
    TemplateResult,
2✔
21
} from '@spectrum-web-components/base';
2✔
22
import {
2✔
23
    classMap,
2✔
24
    ifDefined,
2✔
25
} from '@spectrum-web-components/base/src/directives.js';
2✔
26
import {
2✔
27
    property,
2✔
28
    query,
2✔
29
    state,
2✔
30
} from '@spectrum-web-components/base/src/decorators.js';
2✔
31
import { streamingListener } from '@spectrum-web-components/base/src/streaming-listener.js';
2✔
32
import { randomID } from '@spectrum-web-components/shared/src/random-id.js';
2✔
33

2✔
34
import { WithSWCResizeObserver } from './types';
2✔
35

2✔
36
import styles from './split-view.css.js';
2✔
37

2✔
38
const DEFAULT_MAX_SIZE = 3840;
2✔
39

2✔
40
const SPLITTERSIZE = 2;
2✔
41

2✔
42
const ARROW_KEY_CHANGE_VALUE = 10;
2✔
43

2✔
44
const PAGEUPDOWN_KEY_CHANGE_VALUE = 50;
2✔
45

2✔
46
const COLLAPSE_THREASHOLD = 50;
2✔
47

2✔
48
/**
2✔
49
 * @element sp-split-view
2✔
50
 *
2✔
51
 * @slot Two sibling elements to be sized by the element attritubes
2✔
52
 * @fires change - Announces the new position of the splitter
2✔
53
 */
2✔
54
export class SplitView extends SpectrumElement {
2✔
55
    public static override get styles(): CSSResultArray {
2✔
56
        return [styles];
2✔
57
    }
2✔
58

2✔
59
    @state()
2✔
60
    public controlledEl?: HTMLElement;
2✔
61

2✔
62
    @property({ type: Boolean, reflect: true })
2✔
63
    public vertical = false;
2✔
64

2✔
65
    @property({ type: Boolean, reflect: true })
2✔
66
    public resizable = false;
2✔
67

2✔
68
    @property({ type: Boolean, reflect: true })
2✔
69
    public collapsible = false;
2✔
70

2✔
71
    /** The minimum size of the primary pane */
2✔
72
    @property({ type: Number, attribute: 'primary-min' })
2✔
73
    public primaryMin = 0;
2✔
74

2✔
75
    /** The maximum size of the primary pane */
2✔
76
    @property({ type: Number, attribute: 'primary-max' })
2✔
77
    public primaryMax = DEFAULT_MAX_SIZE;
2✔
78

2✔
79
    /**
2✔
80
     * The start size of the primary pane, can be a real pixel number|string, percentage or "auto"
2✔
81
     * For example: "100", "120px", "75%" or "auto" are valid values
2✔
82
     * @type {number | number + "px" | number + "%" | "auto"}
2✔
83
     * @attr
2✔
84
     */
2✔
85
    @property({ type: String, attribute: 'primary-size' })
2✔
86
    public primarySize?: string;
2✔
87

2✔
88
    /** The minimum size of the secondary pane */
2✔
89
    @property({ type: Number, attribute: 'secondary-min' })
2✔
90
    public secondaryMin = 0;
2✔
91

2✔
92
    /** The maximum size of the secondary pane */
2✔
93
    @property({ type: Number, attribute: 'secondary-max' })
2✔
94
    public secondaryMax = DEFAULT_MAX_SIZE;
2✔
95

2✔
96
    /** The current splitter position of split-view */
2✔
97
    @property({ type: Number, reflect: true, attribute: 'splitter-pos' })
2✔
98
    public splitterPos?: number;
2✔
99

2✔
100
    /** The current size of first pane of split-view */
2✔
101
    @property({ type: String, attribute: false })
2✔
102
    private firstPaneSize = 'auto';
2✔
103

2✔
104
    /** Sets the `aria-label` on the splitter component */
2✔
105
    @property()
2✔
106
    public label?: string;
2✔
107

2✔
108
    @property({ type: Boolean, attribute: false })
2✔
109
    private enoughChildren = false;
2✔
110

2✔
111
    @property({ type: Number })
2✔
112
    private viewSize = 0;
2✔
113

2✔
114
    @query('slot')
2✔
115
    private paneSlot!: HTMLSlotElement;
2✔
116

2✔
117
    @query('#splitter')
2✔
118
    private splitter!: HTMLDivElement;
2✔
119

2✔
120
    private offset = 0;
2✔
121

2✔
122
    private minPos = 0;
2✔
123

2✔
124
    private maxPos = DEFAULT_MAX_SIZE;
2✔
125

2✔
126
    private observer?: WithSWCResizeObserver['ResizeObserver'];
2✔
127

2✔
128
    private rect?: DOMRect;
2✔
129

2✔
130
    private _splitterSize?: number;
2✔
131

2✔
132
    public constructor() {
2✔
133
        super();
27✔
134
        const RO = (window as unknown as WithSWCResizeObserver).ResizeObserver;
27✔
135
        if (RO) {
27✔
136
            this.observer = new RO(() => {
27✔
137
                this.rect = undefined;
13✔
138
                this.updateMinMax();
13✔
139
            });
27✔
140
        }
27✔
141
    }
27✔
142

2✔
143
    public override connectedCallback(): void {
2✔
144
        super.connectedCallback();
27✔
145
        this.observer?.observe(this);
27!
146
    }
27✔
147

2✔
148
    public override disconnectedCallback(): void {
2✔
149
        this.observer?.unobserve(this);
27!
150
        super.disconnectedCallback();
27✔
151
    }
27✔
152

2✔
153
    /**
2✔
154
     * @private
2✔
155
     **/
2✔
156
    public get splitterSize(): number {
2✔
157
        if (!this._splitterSize) {
262✔
158
            this._splitterSize =
26✔
159
                (this.splitter &&
26!
160
                    Math.round(
×
161
                        parseFloat(
×
162
                            window
×
163
                                .getComputedStyle(this.splitter)
×
164
                                .getPropertyValue(
×
165
                                    this.vertical ? 'height' : 'width'
×
166
                                )
×
167
                        )
×
UNCOV
168
                    )) ||
×
169
                SPLITTERSIZE;
26✔
170
        }
26✔
171
        return this._splitterSize;
262✔
172
    }
262✔
173

2✔
174
    protected override render(): TemplateResult {
2✔
175
        const splitterClasses = {
130✔
176
            'is-resized-start': this.splitterPos === this.minPos,
130✔
177
            'is-resized-end': (this.splitterPos &&
130✔
178
                this.splitterPos > this.splitterSize &&
68✔
179
                this.splitterPos === this.maxPos) as boolean,
68✔
180
            'is-collapsed-start': this.splitterPos === 0,
130✔
181
            'is-collapsed-end': (this.splitterPos &&
130✔
182
                this.splitterPos >=
68✔
183
                    Math.max(
68✔
184
                        this.splitterSize,
68✔
185
                        this.viewSize - this.splitterSize
68✔
186
                    )) as boolean,
68✔
187
        };
130✔
188
        const label = this.resizable
130✔
189
            ? this.label || 'Resize the panels'
113✔
190
            : undefined;
17✔
191

130✔
192
        return html`
130✔
193
            <slot
130✔
194
                id=${ifDefined(
130✔
195
                    this.resizable ? this.controlledEl?.id : undefined
130✔
196
                )}
130✔
197
                @slotchange=${this.onContentSlotChange}
130✔
198
                style="--spectrum-split-view-first-pane-size: ${this
130✔
199
                    .firstPaneSize}"
130✔
200
            ></slot>
130✔
201
            ${this.enoughChildren
130✔
202
                ? html`
101✔
203
                      <div
101✔
204
                          id="splitter"
101✔
205
                          class=${classMap(splitterClasses)}
101✔
206
                          role="separator"
101✔
207
                          aria-controls=${ifDefined(
101✔
208
                              this.resizable ? this.controlledEl?.id : undefined
101✔
209
                          )}
101✔
210
                          aria-label=${ifDefined(label)}
101✔
211
                          aria-orientation=${this.vertical
101✔
212
                              ? 'horizontal'
26✔
213
                              : 'vertical'}
101✔
214
                          aria-valuenow=${Math.round(
101✔
215
                              (parseFloat(this.firstPaneSize) / this.viewSize) *
101✔
216
                                  100
101✔
217
                          )}
101✔
218
                          tabindex=${ifDefined(
101✔
219
                              this.resizable ? '0' : undefined
101✔
220
                          )}
101✔
221
                          @keydown=${this.onKeydown}
101✔
222
                          ${streamingListener({
101✔
223
                              start: ['pointerdown', this.onPointerdown],
101✔
224
                              streamInside: ['pointermove', this.onPointermove],
101✔
225
                              end: [
101✔
226
                                  [
101✔
227
                                      'pointerup',
101✔
228
                                      'pointercancel',
101✔
229
                                      'pointerleave',
101✔
230
                                  ],
101✔
231
                                  this.onPointerup,
101✔
232
                              ],
101✔
233
                          })}
101✔
234
                      >
101✔
235
                          ${this.resizable
101✔
236
                              ? html`
91✔
237
                                    <div id="gripper"></div>
10✔
238
                                `
10✔
239
                              : nothing}
101✔
240
                      </div>
29✔
241
                  `
29✔
242
                : nothing}
130✔
243
        `;
130✔
244
    }
130✔
245

2✔
246
    private controlledElIDApplied = false;
2✔
247

2✔
248
    private onContentSlotChange(
2✔
249
        event: Event & { target: HTMLSlotElement }
29✔
250
    ): void {
29✔
251
        if (this.controlledEl && this.controlledElIDApplied) {
29!
252
            this.controlledEl.removeAttribute('id');
×
253
            this.controlledElIDApplied = false;
×
UNCOV
254
        }
×
255
        this.controlledEl = event.target.assignedElements()[0] as HTMLElement;
29✔
256
        if (this.controlledEl && !this.controlledEl.id) {
29✔
257
            this.controlledEl.id = `${this.tagName.toLowerCase()}-${randomID()}`;
25✔
258
            this.controlledElIDApplied = true;
25✔
259
        }
25✔
260
        this.enoughChildren = this.children.length > 1;
29✔
261
        this.checkResize();
29✔
262
    }
29✔
263

2✔
264
    private onPointerdown(event: PointerEvent): void {
2✔
265
        if (!this.resizable || (event.button && event.button !== 0)) {
11✔
266
            event.preventDefault();
2✔
267
            return;
2✔
268
        }
2✔
269
        this.splitter.setPointerCapture(event.pointerId);
9✔
270
        this.offset = this.getOffset();
9✔
271
    }
11✔
272

2✔
273
    private onPointermove(event: PointerEvent): void {
2✔
274
        event.preventDefault();
19✔
275
        let pos =
19✔
276
            this.vertical || this.isLTR
19✔
277
                ? this.getPosition(event) - this.offset
16✔
278
                : this.offset - this.getPosition(event);
3✔
279
        if (this.collapsible && pos < this.minPos - COLLAPSE_THREASHOLD) {
19✔
280
            pos = 0;
2✔
281
        }
2✔
282
        if (this.collapsible && pos > this.maxPos + COLLAPSE_THREASHOLD) {
19✔
283
            pos = this.viewSize - this.splitterSize;
1✔
284
        }
1✔
285
        this.updatePosition(pos);
19✔
286
    }
19✔
287

2✔
288
    private onPointerup(event: PointerEvent): void {
2✔
289
        this.splitter.releasePointerCapture(event.pointerId);
7✔
290
    }
7✔
291

2✔
292
    private getOffset(): number {
2✔
293
        if (!this.rect) {
9✔
294
            this.rect = this.getBoundingClientRect();
9✔
295
        }
9✔
296
        const offsetX = this.isLTR ? this.rect.left : this.rect.right;
9✔
297
        return this.vertical ? this.rect.top : offsetX;
9✔
298
    }
9✔
299

2✔
300
    private getPosition(event: PointerEvent): number {
2✔
301
        return this.vertical ? event.clientY : event.clientX;
19✔
302
    }
19✔
303

2✔
304
    private movePosition(event: KeyboardEvent, offset: number): void {
2✔
305
        event.preventDefault();
21✔
306
        if (this.splitterPos !== undefined) {
21✔
307
            this.updatePosition(this.splitterPos + offset);
21✔
308
        }
21✔
309
    }
21✔
310

2✔
311
    private onKeydown(event: KeyboardEvent): void {
2✔
312
        if (!this.resizable) {
33✔
313
            return;
1✔
314
        }
1✔
315
        let direction = 0;
32✔
316
        const isLTRorVertical = this.isLTR || this.vertical;
33✔
317
        switch (event.key) {
33✔
318
            case 'Home':
33✔
319
                event.preventDefault();
4✔
320
                this.updatePosition(this.collapsible ? 0 : this.minPos);
4✔
321
                return;
4✔
322
            case 'End':
33✔
323
                event.preventDefault();
4✔
324
                this.updatePosition(
4✔
325
                    this.collapsible
4✔
326
                        ? this.viewSize - this.splitterSize
1✔
327
                        : this.maxPos
3✔
328
                );
4✔
329
                return;
4✔
330
            case 'ArrowLeft':
33✔
331
                direction = isLTRorVertical ? -1 : 1;
5✔
332
                break;
5✔
333
            case 'ArrowRight':
33✔
334
                direction = isLTRorVertical ? 1 : -1;
4✔
335
                break;
4✔
336
            case 'ArrowUp':
33✔
337
                direction = this.vertical ? -1 : 1;
3✔
338
                break;
3✔
339
            case 'ArrowDown':
33✔
340
                direction = this.vertical ? 1 : -1;
3✔
341
                break;
3✔
342
            case 'PageUp':
33✔
343
                direction = this.vertical ? -1 : 1;
3✔
344
                break;
3✔
345
            case 'PageDown':
33✔
346
                direction = this.vertical ? 1 : -1;
3✔
347
                break;
3✔
348
        }
33✔
349
        if (direction !== 0) {
33✔
350
            const moveBy = event.key.startsWith('Page')
21✔
351
                ? PAGEUPDOWN_KEY_CHANGE_VALUE
6✔
352
                : ARROW_KEY_CHANGE_VALUE;
15✔
353
            this.movePosition(event, moveBy * direction);
21✔
354
        }
21✔
355
    }
33✔
356

2✔
357
    private async checkResize(): Promise<void> {
2✔
358
        if (!this.enoughChildren) {
57✔
359
            return;
29✔
360
        }
29✔
361
        this.updateMinMax();
28✔
362
        if (this.splitterPos === undefined) {
57✔
363
            const startPos = await this.calcStartPos();
27✔
364
            this.updatePosition(startPos);
27✔
365
        }
27✔
366
    }
57✔
367

2✔
368
    private updateMinMax(): void {
2✔
369
        this.viewSize = this.vertical ? this.offsetHeight : this.offsetWidth;
41✔
370
        this.minPos = Math.max(
41✔
371
            this.primaryMin,
41✔
372
            this.viewSize - this.secondaryMax
41✔
373
        );
41✔
374
        this.maxPos = Math.min(
41✔
375
            this.primaryMax,
41✔
376
            this.viewSize - Math.max(this.secondaryMin, this.splitterSize)
41✔
377
        );
41✔
378
    }
41✔
379

2✔
380
    private updatePosition(x: number): void {
2✔
381
        let pos = this.getLimitedPosition(x);
75✔
382
        if (this.collapsible && x <= 0) {
75✔
383
            pos = 0;
3✔
384
        }
3✔
385
        if (
75✔
386
            this.collapsible &&
75✔
387
            x > this.maxPos &&
13✔
388
            x >= this.viewSize - this.splitterSize
5✔
389
        ) {
75✔
390
            pos = this.viewSize - this.splitterSize;
3✔
391
        }
3✔
392
        if (pos !== this.splitterPos) {
75✔
393
            this.splitterPos = pos;
73✔
394
            this.dispatchChangeEvent();
73✔
395
        }
73✔
396
    }
75✔
397

2✔
398
    private getLimitedPosition(input: number): number {
2✔
399
        if (input <= this.minPos) {
76✔
400
            return this.minPos;
13✔
401
        }
13✔
402
        if (input >= this.maxPos) {
76✔
403
            return this.maxPos;
15✔
404
        }
15✔
405
        return Math.max(this.minPos, Math.min(this.maxPos, input));
48✔
406
    }
76✔
407

2✔
408
    private async calcStartPos(): Promise<number> {
2✔
409
        if (
27✔
410
            this.primarySize !== undefined &&
27✔
411
            /^\d+(px)?$/.test(this.primarySize)
7✔
412
        ) {
27✔
413
            return parseInt(this.primarySize, 10);
5✔
414
        }
5✔
415
        if (this.primarySize !== undefined && /^\d+%$/.test(this.primarySize)) {
27✔
416
            return (parseInt(this.primarySize, 10) * this.viewSize) / 100;
1✔
417
        }
1✔
418
        if (this.primarySize === 'auto') {
27✔
419
            this.firstPaneSize = 'auto';
1✔
420
            const nodes = this.paneSlot.assignedNodes({ flatten: true });
1✔
421
            const firstEl = nodes.find(
1✔
422
                (node) => node instanceof HTMLElement
1✔
423
            ) as LitElement;
1✔
424
            if (typeof firstEl.updateComplete !== 'undefined') {
1!
425
                await firstEl.updateComplete;
×
UNCOV
426
            }
×
427
            if (firstEl) {
1✔
428
                const size = window
1✔
429
                    .getComputedStyle(firstEl)
1✔
430
                    .getPropertyValue(this.vertical ? 'height' : 'width');
1!
431
                const size_i = parseFloat(size);
1✔
432
                if (!isNaN(size_i)) {
1✔
433
                    return this.getLimitedPosition(Math.ceil(size_i));
1✔
434
                }
1✔
435
            }
1✔
436
        }
1✔
437
        return this.viewSize / 2;
20✔
438
    }
27✔
439

2✔
440
    private dispatchChangeEvent(): void {
2✔
441
        const changeEvent = new Event('change', {
73✔
442
            bubbles: true,
73✔
443
            composed: true,
73✔
444
        });
73✔
445
        this.dispatchEvent(changeEvent);
73✔
446
    }
73✔
447

2✔
448
    protected override willUpdate(changed: PropertyValues): void {
2✔
449
        if (!this.hasUpdated || changed.has('primarySize')) {
130✔
450
            this.splitterPos = undefined;
28✔
451
            this.checkResize();
28✔
452
        }
28✔
453
        if (
130✔
454
            changed.has('splitterPos') &&
130✔
455
            this.splitterPos !== undefined &&
74✔
456
            this.enoughChildren
73✔
457
        ) {
130✔
458
            this.firstPaneSize = `${Math.round(this.splitterPos)}px`;
73✔
459
        }
73✔
460
    }
130✔
461
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc