@@ -87,67 +87,28 @@ suite('copy-cmd', () => {
8787 document . body . removeChild ( el ) ;
8888 } ) ;
8989
90- test ( 'appends the copy action to the accessible name without hiding the command' , async ( ) => {
90+ test ( 'describes the copy action via a title without hiding the command' , async ( ) => {
9191 const el = await mount ( 'npm create webjs@latest my-app' ) ;
9292 const target = el . querySelector ( '[data-copy-text]' ) ;
9393 // The command stays the accessible NAME (slotted text, no aria-label)...
9494 assert . equal ( target . getAttribute ( 'aria-label' ) , null , 'no aria-label overrides the command name' ) ;
9595 assert . ok ( target . textContent . includes ( 'npm create webjs@latest my-app' ) , 'the command is the accessible name' ) ;
96- // ...and a visually-hidden phrase inside the same target appends the copy
97- // ACTION to the accessible name, so both are announced.
98- const hint = target . querySelector ( '.sr-only' ) ;
99- assert . ok ( hint , 'a visually-hidden description sits inside the click target' ) ;
100- assert . ok ( / c o p y / i. test ( hint . textContent ) && / c l i p b o a r d / i. test ( hint . textContent ) ,
101- 'the hidden text describes the copy-to-clipboard action' ) ;
102- assert . ok ( target . textContent . includes ( 'Copy command to clipboard' ) ,
103- 'the description is part of the accessible name, after the command' ) ;
104- // Self-contained: the description must not depend on an element rendered
105- // elsewhere (a layout-provided target would dangle wherever copy-cmd is
106- // used outside that layout, e.g. an error boundary or another app).
96+ // ...and the title supplies the copy ACTION as the accessible description
97+ // (plus a native hover tooltip). A title needs no id, which is the point:
98+ // the old aria-describedby needed a document-unique id, the only SSR-safe
99+ // source of one is a module-scope counter, and the counter made every
100+ // render emit different bytes, killing the page ETag (#1127).
101+ assert . equal ( target . getAttribute ( 'title' ) , 'Copy command to clipboard' ,
102+ 'the title describes the copy-to-clipboard action' ) ;
107103 assert . equal ( target . getAttribute ( 'aria-describedby' ) , null ,
108- 'no aria-describedby reference to an element outside the component' ) ;
104+ 'no id-based description reference remains' ) ;
105+ // The description is an attribute, not text, so the target's text is
106+ // EXACTLY the command: selections and _copy cannot pick up anything else.
107+ assert . equal ( target . textContent . trim ( ) , 'npm create webjs@latest my-app' ,
108+ 'the click target contains only the command text' ) ;
109109 document . body . removeChild ( el ) ;
110110 } ) ;
111111
112- test ( 'copying excludes the visually-hidden description' , async ( ) => {
113- // The description shares the click target with the command, so a naive
114- // textContent read would put "Copy command to clipboard" on the clipboard.
115- const el = await mount ( 'npm create webjs@latest my-app' ) ;
116- el . querySelector ( '[data-copy-text]' ) . click ( ) ;
117- await tick ( ) ;
118- assert . equal ( written , 'npm create webjs@latest my-app' ,
119- 'only the command is copied, not the hidden description' ) ;
120- el . remove ( ) ;
121- } ) ;
122-
123- test ( 'the hidden description is excluded from a manual text selection' , async ( ) => {
124- // The copy BUTTON is not the only way a visitor copies a command: plenty
125- // triple-click the line and hit Ctrl+C. The description sits in the same
126- // inline container as the command, so it needs user-select:none or it
127- // pastes along with it.
128- //
129- // This asserts the MECHANISM rather than a selection, deliberately. A
130- // programmatic Range ignores user-select entirely (selectNodeContents will
131- // happily return the hidden text), so a range-based assertion here would
132- // fail while real browsers behave correctly, testing nothing useful. The
133- // actual behaviour was verified against a real browser: a genuine
134- // triple-click on the hero command yields exactly
135- // "npm create webjs@latest my-app", and Ctrl+A over the whole document
136- // does not pick the description up either.
137- // Asserted as a class rather than a computed style because this harness
138- // does not load the compiled Tailwind stylesheet, so every utility computes
139- // to its initial value here. The rest of this file asserts sr-only the same
140- // way for the same reason.
141- const el = await mount ( 'npm create webjs@latest my-app' ) ;
142- const hint = el . querySelector ( '[data-copy-text] .sr-only' ) ;
143- assert . ok ( hint . className . includes ( 'select-none' ) ,
144- 'the hidden description opts out of text selection' ) ;
145- const command = el . querySelector ( '[data-copy-source]' ) ;
146- assert . ok ( command , 'the command has its own wrapper so _copy can read it alone' ) ;
147- assert . ok ( ! command . className . includes ( 'select-none' ) , 'the command itself stays selectable' ) ;
148- el . remove ( ) ;
149- } ) ;
150-
151112 test ( 'renders no generated ids, so repeated renders are byte-identical' , async ( ) => {
152113 // The regression guard for #1127. Any per-render-varying value in the
153114 // component's output (a counter, a timestamp, a random id) changes the
0 commit comments