UNPKG

chrome-devtools-frontend

Version:
651 lines (582 loc) • 29.2 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import {renderElementIntoDOM} from '../../testing/DOMHelpers.js'; import { createTarget, describeWithEnvironment, describeWithLocale, getGetHostConfigStub, } from '../../testing/EnvironmentHelpers.js'; import {expectCall} from '../../testing/ExpectStubCall.js'; import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js'; import {getMatchedStyles} from '../../testing/StyleHelpers.js'; import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Elements from './elements.js'; describe('StylesSidebarPane', () => { let node: SDK.DOMModel.DOMNode; beforeEach(() => { node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node); }); describeWithMockConnection('StylesSidebarPane', () => { beforeEach(() => { const target = createTarget(); const cssModel = target.model(SDK.CSSModel.CSSModel); sinon.stub(Elements.ComputedStyleModel.ComputedStyleModel.prototype, 'cssModel').returns(cssModel); }); it('unescapes CSS strings', () => { assert.strictEqual( Elements.StylesSidebarPane.unescapeCssString( String.raw`"I\F1 t\EB rn\E2 ti\F4 n\E0 liz\E6 ti\F8 n\2603 \1F308 can be \t\r\ic\k\y"`), '"I\xF1t\xEBrn\xE2ti\xF4n\xE0liz\xE6ti\xF8n\u2603\u{1F308} can be tricky"'); assert.strictEqual( Elements.StylesSidebarPane.unescapeCssString(String.raw`"_\DBFF_\\DBFF_\\\DBFF_\\\\DBFF_\\\\\DBFF_"`), '"_\uFFFD_\\DBFF_\\\\DBFF_\\\\\\DBFF_\\\\\\\\DBFF_"'); assert.strictEqual( Elements.StylesSidebarPane.unescapeCssString(String.raw`"\0_\DBFF_\DFFF_\110000"`), '"\uFFFD_\uFFFD_\uFFFD_\uFFFD"', 'U+0000, lone surrogates, and values above U+10FFFF should become U+FFFD'); assert.strictEqual( Elements.StylesSidebarPane.unescapeCssString(String.raw`"_\D83C\DF08_"`), '"_\uFFFD\uFFFD_"', 'surrogates should not be combined'); assert.strictEqual( Elements.StylesSidebarPane.unescapeCssString('"_\\41\n_\\41\t_\\41\x20_"'), '"_A_A_A_"', 'certain trailing whitespace characters should be consumed as part of the escape sequence'); }); it('escapes URL as CSS comments', () => { assert.strictEqual(Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/'), 'https://abc.com/'); assert.strictEqual(Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/'), 'https://abc.com/*/'); assert.strictEqual( Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/?q=*'), 'https://abc.com/*/?q=*'); assert.strictEqual( Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/?q=*/'), 'https://abc.com/*/?q=*%2F'); assert.strictEqual( Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/?q=*/#hash'), 'https://abc.com/*/?q=*%2F#hash'); }); describe('rebuildSectionsForMatchedStyleRulesForTest', () => { it('should add @position-try section', async () => { const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node: sinon.createStubInstance(SDK.DOMModel.DOMNode), positionTryRules: [{ name: {text: '--try-one'}, origin: Protocol.CSS.StyleSheetOrigin.Regular, style: { cssProperties: [{name: 'bottom', value: 'anchor(--anchor-name bottom)'}], shorthandEntries: [], }, active: false, }], }); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks, 2); assert.strictEqual(sectionBlocks[1].titleElement()?.textContent, '@position-try --try-one'); assert.lengthOf(sectionBlocks[1].sections, 1); assert.instanceOf(sectionBlocks[1].sections[0], Elements.StylePropertiesSection.PositionTryRuleSection); }); }); it('should add @font-palette-values section to the end', async () => { const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node: sinon.createStubInstance(SDK.DOMModel.DOMNode), fontPaletteValuesRule: { fontPaletteName: {text: '--palette'}, origin: Protocol.CSS.StyleSheetOrigin.Regular, style: { cssProperties: [{name: 'font-family', value: 'Bixa'}, {name: 'override-colors', value: '0 red'}], shorthandEntries: [], }, }, }); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks, 2); assert.strictEqual(sectionBlocks[1].titleElement()?.textContent, '@font-palette-values --palette'); assert.lengthOf(sectionBlocks[1].sections, 1); assert.instanceOf(sectionBlocks[1].sections[0], Elements.StylePropertiesSection.FontPaletteValuesRuleSection); }); describe('Animation styles', () => { function mockGetAnimatedComputedStyles(response: Partial<Protocol.CSS.GetAnimatedStylesForNodeResponse>) { setMockConnectionResponseHandler('CSS.getAnimatedStylesForNode', () => response); } let hostConfigStub: sinon.SinonStub; beforeEach(() => { sinon.stub(Common.Linkifier.Linkifier, 'linkify').returns(Promise.resolve(document.createTextNode('link'))); hostConfigStub = getGetHostConfigStub({ devToolsAnimationStylesInStylesTab: { enabled: true, }, }); }); afterEach(() => { hostConfigStub.restore(); }); it('should render transition & animation styles in the styles tab', async () => { const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node: sinon.createStubInstance(SDK.DOMModel.DOMNode), animationStylesPayload: [ { name: '--animation-name', style: { cssProperties: [{ name: 'background-color', value: 'blue', }], shorthandEntries: [], }, }, { style: { cssProperties: [{ name: 'color', value: 'blue', }], shorthandEntries: [], }, }, ], transitionsStylePayload: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, inheritedAnimatedPayload: [], }); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 3); assert.strictEqual(sectionBlocks[0].sections[0].headerText(), 'transitions style'); assert.strictEqual(sectionBlocks[0].sections[1].headerText(), '--animation-name animation'); assert.strictEqual(sectionBlocks[0].sections[2].headerText(), 'animation style'); }); describe('should auto update animated style sections when onComputedStyleChanged called', () => { describe('transition styles', () => { it('should trigger re-render when there was no transition style before', async () => { mockGetAnimatedComputedStyles({ transitionsStyle: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, transitionsStylePayload: null, }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 0); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; sinon.assert.called(resetUpdateSpy); }); it('should update value only when there was a transition style before', async () => { mockGetAnimatedComputedStyles({ transitionsStyle: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, transitionsStylePayload: { cssProperties: [{ name: 'color', value: 'blue', }], shorthandEntries: [], }, }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 1); assert.include( sectionBlocks[0].sections[0].propertiesTreeOutline.contentElement.textContent, 'color: blue;'); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; assert.include( sectionBlocks[0].sections[0].propertiesTreeOutline.contentElement.textContent, 'color: red;'); sinon.assert.notCalled(resetUpdateSpy); }); }); describe('animation styles', () => { it('should trigger re-render when there was no animation style before', async () => { mockGetAnimatedComputedStyles({ animationStyles: [{ name: '--animation', style: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, }], }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, animationStylesPayload: [], }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 0); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; sinon.assert.called(resetUpdateSpy); }); it('should trigger re-render when there is no animation style after', async () => { mockGetAnimatedComputedStyles({ animationStyles: [], }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, animationStylesPayload: [{ style: { cssProperties: [{ name: 'color', value: 'blue', }], shorthandEntries: [], }, }], }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 1); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; sinon.assert.called(resetUpdateSpy); }); it('should update value only when there was the animation style before', async () => { mockGetAnimatedComputedStyles({ animationStyles: [{ style: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, }], }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, animationStylesPayload: [{ style: { cssProperties: [{ name: 'color', value: 'blue', }], shorthandEntries: [], }, }], }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 1); assert.include( sectionBlocks[0].sections[0].propertiesTreeOutline.contentElement.textContent, 'color: blue;'); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; assert.include( sectionBlocks[0].sections[0].propertiesTreeOutline.contentElement.textContent, 'color: red;'); sinon.assert.notCalled(resetUpdateSpy); }); }); describe('inherited animated styles', () => { describe('transition styles', () => { it('should trigger re-render when there was no inherited transition style but there is a new one now', async () => { mockGetAnimatedComputedStyles({ inherited: [{ transitionsStyle: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, }], }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; node.parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode); const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane( new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest( matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 0); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; sinon.assert.called(resetUpdateSpy); }); it('should not trigger re-render when there was no inherited transition style and the new one does not contain inherited property', async () => { mockGetAnimatedComputedStyles({ inherited: [{ transitionsStyle: { cssProperties: [{ name: 'background-color', value: 'red', }], shorthandEntries: [], }, }], }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; node.parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode); const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane( new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest( matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[0].sections, 0); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; sinon.assert.notCalled(resetUpdateSpy); }); it('should update value only when there is no new inherited transition style and the value is updated', async () => { mockGetAnimatedComputedStyles({ inherited: [{ transitionsStyle: { cssProperties: [{ name: 'color', value: 'red', }], shorthandEntries: [], }, }], }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); node.id = 1 as Protocol.DOM.NodeId; node.parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode); const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane( new Elements.ComputedStyleModel.ComputedStyleModel()); const resetUpdateSpy = sinon.spy(stylesSidebarPane, 'scheduleResetUpdateIfNotEditingCalledForTest'); const matchedStyles = await getMatchedStyles({ cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, node, inheritedPayload: [{ matchedCSSRules: [], }], inheritedAnimatedPayload: [{ transitionsStyle: { cssProperties: [{ name: 'color', value: 'blue', }], shorthandEntries: [], }, }], }); stylesSidebarPane.setMatchedStylesForTest(matchedStyles); const sectionBlocks = await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest( matchedStyles, new Map(), new Map()); assert.lengthOf(sectionBlocks[1].sections, 1); assert.include( sectionBlocks[1].sections[0].propertiesTreeOutline.contentElement.textContent, 'color: blue;'); const handledComputedStyleChanged = expectCall(sinon.stub(stylesSidebarPane, 'handledComputedStyleChangedForTest')); stylesSidebarPane.onComputedStyleChanged(); await handledComputedStyleChanged; assert.include( sectionBlocks[1].sections[0].propertiesTreeOutline.contentElement.textContent, 'color: red;'); sinon.assert.notCalled(resetUpdateSpy); }); }); }); }); }); }); describe('IdleCallbackManager', () => { // IdleCallbackManager delegates work using requestIdleCallback, which does not generally execute requested callbacks // in order. This test verifies that callbacks do happen in order even if timeouts are run out. it('schedules callbacks in order', async () => { // Override the default timeout with a very short one class QuickIdleCallbackManager extends Elements.StylesSidebarPane.IdleCallbackManager { protected override scheduleIdleCallback(_: number): void { super.scheduleIdleCallback(1); } } const timeout = (time: number) => new Promise<void>(resolve => setTimeout(resolve, time)); const elements: number[] = []; const callbacks = new QuickIdleCallbackManager(); callbacks.schedule(() => elements.push(0)); callbacks.schedule(() => elements.push(1)); callbacks.schedule(() => elements.push(2)); callbacks.schedule(() => elements.push(3)); await timeout(10); callbacks.schedule(() => elements.push(4)); callbacks.schedule(() => elements.push(5)); callbacks.schedule(() => elements.push(6)); callbacks.schedule(() => elements.push(7)); await timeout(10); await callbacks.awaitDone(); assert.deepEqual(elements, [0, 1, 2, 3, 4, 5, 6, 7]); }); }); describeWithLocale('CSSPropertyPrompt', () => { const CSSPropertyPrompt = Elements.StylesSidebarPane.CSSPropertyPrompt; const CSS_VARIABLES_FOR_TEST: Record<string, string> = { '--rgb-color': 'rgb(0 0 0)', '--wide-gamut-color': 'lch(0 0 0)', }; const mockTreeItem = { property: { name: 'color', }, node() { return { isSVGNode() { return false; }, domModel() { return { cssModel() { return { getComputedStyle() { return new Map<string, string>(); }, }; }, }; }, }; }, matchedStyles() { return { availableCSSVariables(): string[] { return ['--rgb-color', '--wide-gamut-color']; }, computeCSSVariable(_: unknown, completion: string): {value: string, declaration: null} | undefined { return {value: CSS_VARIABLES_FOR_TEST[completion], declaration: null}; }, }; }, } as unknown as Elements.StylePropertyTreeElement.StylePropertyTreeElement; const noop = () => {}; describeWithEnvironment('value autocompletion', () => { it('shows autocomplete item with color swatch for CSS variables with RGB color', async () => { const attachedElement = document.createElement('div'); renderElementIntoDOM(attachedElement); const cssPropertyPrompt = new CSSPropertyPrompt(mockTreeItem, false); cssPropertyPrompt.attachAndStartEditing(attachedElement, noop); const spyObj = sinon.spy(cssPropertyPrompt.suggestBoxForTest()); cssPropertyPrompt.setText('var(--rgb'); await cssPropertyPrompt.complete(true); const colorCompletions = spyObj?.updateSuggestions.firstCall.args[1]; const renderedElement = colorCompletions?.[0].subtitleRenderer?.(); assert.instanceOf(renderedElement, InlineEditor.ColorSwatch.ColorSwatch); }); it('shows autocomplete item with color swatch for CSS variables with wide gamut color', async () => { const attachedElement = document.createElement('div'); renderElementIntoDOM(attachedElement); const cssPropertyPrompt = new CSSPropertyPrompt(mockTreeItem, false); cssPropertyPrompt.attachAndStartEditing(attachedElement, noop); const spyObj = sinon.spy(cssPropertyPrompt.suggestBoxForTest()); cssPropertyPrompt.setText('var(--wide'); await cssPropertyPrompt.complete(true); const colorCompletions = spyObj?.updateSuggestions.firstCall.args[1]; const renderedElement = colorCompletions?.[0].subtitleRenderer?.(); assert.instanceOf(renderedElement, InlineEditor.ColorSwatch.ColorSwatch); }); it('shows autocomplete property names for CSS aliases', async () => { const attachedElement = document.createElement('div'); renderElementIntoDOM(attachedElement); const cssPropertyPrompt = new CSSPropertyPrompt(mockTreeItem, true); cssPropertyPrompt.attachAndStartEditing(attachedElement, noop); const spyObj = sinon.spy(cssPropertyPrompt.suggestBoxForTest()); cssPropertyPrompt.setText('word-wra'); await cssPropertyPrompt.complete(true); const completions = spyObj?.updateSuggestions.firstCall.args[1]; assert.strictEqual(completions?.[0].text, 'word-wrap'); assert.strictEqual(completions?.[1].text, 'overflow-wrap'); assert.strictEqual(completions?.[1].subtitle, '= word-wrap'); }); }); }); });