• 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

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

64✔
13
import {
64✔
14
    CSSResultArray,
64✔
15
    html,
64✔
16
    PropertyValues,
64✔
17
    TemplateResult,
64✔
18
} from '@spectrum-web-components/base';
64✔
19
import {
64✔
20
    property,
64✔
21
    query,
64✔
22
} from '@spectrum-web-components/base/src/decorators.js';
64✔
23
import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js';
64✔
24
import { Focusable } from '@spectrum-web-components/shared/src/focusable.js';
64✔
25
import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js';
64✔
26
import buttonStyles from './button-base.css.js';
64✔
27

64✔
28
/**
64✔
29
 * @slot - text content to be displayed in the Button element
64✔
30
 * @slot icon - icon element(s) to display at the start of the button
64✔
31
 */
64✔
32
export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
64✔
33
    'sp-overlay,sp-tooltip',
64✔
34
]) {
64✔
35
    public static override get styles(): CSSResultArray {
64✔
36
        return [buttonStyles];
64✔
37
    }
64✔
38

64✔
39
    // TODO we need to document this property for consumers,
64✔
40
    // as it's not a 1:1 equivalent to active
64✔
41
    @property({ type: Boolean, reflect: true })
64✔
42
    public active = false;
64✔
43

64✔
44
    /**
64✔
45
     * The default behavior of the button.
64✔
46
     * Possible values are: `button` (default), `submit`, and `reset`.
64✔
47
     */
64✔
48
    @property({ type: String })
64✔
49
    public type: 'button' | 'submit' | 'reset' = 'button';
64✔
50

64✔
51
    /**
64✔
52
     * HTML anchor element that component clicks by proxy
64✔
53
     */
64✔
54
    @query('.anchor')
64✔
55
    private anchorElement!: HTMLAnchorElement;
64✔
56

64✔
57
    public override get focusElement(): HTMLElement {
64✔
58
        return this;
9,571✔
59
    }
9,571✔
60

64✔
61
    protected get hasLabel(): boolean {
64✔
62
        return this.slotHasContent;
1,874✔
63
    }
1,874✔
64

64✔
65
    protected get buttonContent(): TemplateResult[] {
64✔
66
        const content = [
1,871✔
67
            html`
1,871✔
68
                <slot name="icon" ?icon-only=${!this.hasLabel}></slot>
1,871✔
69
            `,
1,871✔
70
            html`
1,871✔
71
                <span id="label">
1,871✔
72
                    <slot @slotchange=${this.manageTextObservedSlot}></slot>
1,871✔
73
                </span>
1,871✔
74
            `,
1,871✔
75
        ];
1,871✔
76
        return content;
1,871✔
77
    }
1,871✔
78

64✔
79
    constructor() {
64✔
80
        super();
1,395✔
81
        this.proxyFocus = this.proxyFocus.bind(this);
1,395✔
82

1,395✔
83
        this.addEventListener('click', this.handleClickCapture, {
1,395✔
84
            capture: true,
1,395✔
85
        });
1,395✔
86
    }
1,395✔
87

64✔
88
    private handleClickCapture(event: Event): void | boolean {
64✔
89
        if (this.disabled) {
239✔
90
            event.preventDefault();
1✔
91
            event.stopImmediatePropagation();
1✔
92
            event.stopPropagation();
1✔
93
            return false;
1✔
94
        }
1✔
95

238✔
96
        if (this.shouldProxyClick(event as MouseEvent)) {
239✔
97
            return;
10✔
98
        }
10✔
99
    }
239✔
100

64✔
101
    private proxyFocus(): void {
64✔
102
        this.focus();
2✔
103
    }
2✔
104

64✔
105
    private shouldProxyClick(event?: MouseEvent): boolean {
64✔
106
        let handled = false;
238✔
107

238✔
108
        // Don't proxy clicks with modifier keys (Command/Meta, Ctrl, Shift, Alt)
238✔
109
        if (
238✔
110
            event &&
238✔
111
            (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
238✔
112
        ) {
238✔
113
            return false;
2✔
114
        }
2✔
115

236✔
116
        if (this.anchorElement) {
238✔
117
            // Click HTML anchor element by proxy, but only for non-modified clicks
6✔
118
            this.anchorElement.click();
6✔
119
            handled = true;
6✔
120
            // if the button type is `submit` or `reset`
6✔
121
        } else if (this.type !== 'button') {
238✔
122
            // create an HTML Button Element by proxy, click it, and remove it
4✔
123
            const proxy = document.createElement('button');
4✔
124
            proxy.type = this.type;
4✔
125
            this.insertAdjacentElement('afterend', proxy);
4✔
126
            proxy.click();
4✔
127
            proxy.remove();
4✔
128
            handled = true;
4✔
129
        }
4✔
130
        return handled;
236✔
131
    }
238✔
132

64✔
133
    public override renderAnchor(): TemplateResult {
64✔
134
        return html`
30✔
135
            ${this.buttonContent}
30✔
136
            ${super.renderAnchor({
30✔
137
                id: 'button',
30✔
138
                ariaHidden: true,
30✔
139
                className: 'button anchor',
30✔
140
                tabindex: -1,
30✔
141
            })}
30✔
142
        `;
30✔
143
    }
30✔
144

64✔
145
    protected renderButton(): TemplateResult {
64✔
146
        return html`
1,977✔
147
            ${this.buttonContent}
1,977✔
148
        `;
1,977✔
149
    }
1,977✔
150

64✔
151
    protected override render(): TemplateResult {
64✔
152
        return this.href && this.href.length > 0
2,551✔
153
            ? this.renderAnchor()
30✔
154
            : this.renderButton();
2,521✔
155
    }
2,551✔
156

64✔
157
    protected handleKeydown(event: KeyboardEvent): void {
64✔
158
        const { code } = event;
107✔
159
        switch (code) {
107✔
160
            case 'Space':
107✔
161
                event.preventDefault();
9✔
162
                // allows button to activate when `Space` is pressed
9✔
163
                if (typeof this.href === 'undefined') {
9✔
164
                    this.addEventListener('keyup', this.handleKeyup);
9✔
165
                    this.active = true;
9✔
166
                }
9✔
167
                break;
9✔
168
            default:
107✔
169
                break;
98✔
170
        }
107✔
171
    }
107✔
172

64✔
173
    private handleKeypress(event: KeyboardEvent): void {
64✔
174
        const { code } = event;
29✔
175
        switch (code) {
29✔
176
            case 'Enter':
29✔
177
            case 'NumpadEnter':
29✔
178
                // allows button or link to be activated with `Enter` and `NumpadEnter`
28✔
179
                this.click();
28✔
180
                break;
28✔
181
            default:
29✔
182
                break;
1✔
183
        }
29✔
184
    }
29✔
185

64✔
186
    protected handleKeyup(event: KeyboardEvent): void {
64✔
187
        const { code } = event;
10✔
188
        switch (code) {
10✔
189
            case 'Space':
10✔
190
                this.removeEventListener('keyup', this.handleKeyup);
9✔
191
                this.active = false;
9✔
192
                this.click();
9✔
193
                break;
9✔
194
            default:
10✔
195
                break;
1✔
196
        }
10✔
197
    }
10✔
198

64✔
199
    private manageAnchor(): void {
64✔
200
        // for a link
1,409✔
201
        if (this.href && this.href.length > 0) {
1,409✔
202
            // if the role is set to button
25✔
203
            if (
25✔
204
                !this.hasAttribute('role') ||
25✔
205
                this.getAttribute('role') === 'button'
14✔
206
            ) {
25✔
207
                // change role to link
13✔
208
                this.setAttribute('role', 'link');
13✔
209
            }
13✔
210
            // else for a button
25✔
211
        } else {
1,409✔
212
            // if the role is set to link
1,384✔
213
            if (
1,384✔
214
                !this.hasAttribute('role') ||
1,384✔
215
                this.getAttribute('role') === 'link'
532✔
216
            ) {
1,384✔
217
                // change role to button
853✔
218
                this.setAttribute('role', 'button');
853✔
219
            }
853✔
220
        }
1,384✔
221
    }
1,409✔
222

64✔
223
    protected override firstUpdated(changed: PropertyValues): void {
64✔
224
        super.firstUpdated(changed);
1,395✔
225
        if (!this.hasAttribute('tabindex')) {
1,395✔
226
            this.setAttribute('tabindex', '0');
986✔
227
        }
986✔
228
        if (changed.has('label')) {
1,395✔
229
            if (this.label) {
385✔
230
                this.setAttribute('aria-label', this.label);
384✔
231
            } else {
385✔
232
                this.removeAttribute('aria-label');
1✔
233
            }
1✔
234
        }
385✔
235
        this.manageAnchor();
1,395✔
236
        this.addEventListener('keydown', this.handleKeydown);
1,395✔
237
        this.addEventListener('keypress', this.handleKeypress);
1,395✔
238
    }
1,395✔
239

64✔
240
    protected override updated(changed: PropertyValues): void {
64✔
241
        super.updated(changed);
2,622✔
242
        if (changed.has('href')) {
2,622✔
243
            this.manageAnchor();
14✔
244
        }
14✔
245

2,622✔
246
        if (this.anchorElement) {
2,622✔
247
            // Ensure the anchor element is not focusable directly via tab
30✔
248
            this.anchorElement.tabIndex = -1;
30✔
249

30✔
250
            // Make sure it has proper ARIA attributes
30✔
251
            if (!this.anchorElement.hasAttribute('aria-hidden')) {
30!
UNCOV
252
                this.anchorElement.setAttribute('aria-hidden', 'true');
×
UNCOV
253
            }
×
254

30✔
255
            // Set up focus delegation
30✔
256
            this.anchorElement.addEventListener('focus', this.proxyFocus);
30✔
257
        }
30✔
258
    }
2,622✔
259
    protected override update(changes: PropertyValues): void {
64✔
260
        super.update(changes);
2,622✔
261
        if (changes.has('label')) {
2,622✔
262
            if (this.label) {
663✔
263
                this.setAttribute('aria-label', this.label);
662✔
264
            } else {
663✔
265
                this.removeAttribute('aria-label');
1✔
266
            }
1✔
267
        }
663✔
268
    }
2,622✔
269
}
64✔
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