@@ -11,6 +11,19 @@ function uid() {
1111 return Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) ;
1212}
1313
14+ // Task rows collapse to a scannable line at rest (#102). Expand the row to
15+ // reveal the required-link/file, completion-proof, waiting, and assistant
16+ // controls before interacting with them.
17+ async function expandRow ( row ) {
18+ const toggle = row . locator ( '[data-task-expand]' ) . first ( ) ;
19+ if ( await toggle . count ( ) ) {
20+ if ( ( await toggle . getAttribute ( 'aria-expanded' ) ) === 'false' ) {
21+ await toggle . click ( ) ;
22+ }
23+ await expect ( row . locator ( '.task-checklist-details' ) ) . toBeVisible ( ) ;
24+ }
25+ }
26+
1427test . describe ( 'Bundle detail view (issue #27)' , ( ) => {
1528
1629 // ── Scenario: Grace views a bundle with references and bundle links ──
@@ -162,6 +175,9 @@ test.describe('Bundle detail view (issue #27)', () => {
162175 const checkbox = taskRow . locator ( '.task-status-checkbox' ) ;
163176 await expect ( checkbox ) . toBeDisabled ( ) ;
164177
178+ // Expand the collapsed row to reach the required-link editor
179+ await expandRow ( taskRow ) ;
180+
165181 // Required link label
166182 const linkLabel = taskRow . locator ( '.required-link-label' ) ;
167183 await expect ( linkLabel ) . toHaveText ( 'Luma:' ) ;
@@ -745,6 +761,7 @@ test.describe('Bundle detail view (issue #27)', () => {
745761 const sponsorRow = page . locator ( '[data-task-row="' + sponsorTask . id + '"]' ) ;
746762 await expect ( sponsorRow ) . toContainText ( 'Add Sponsorship document link to complete' ) ;
747763 await expect ( sponsorRow . locator ( '[data-task-checkbox="' + sponsorTask . id + '"]' ) ) . toBeDisabled ( ) ;
764+ await expandRow ( sponsorRow ) ;
748765 await sponsorRow . locator ( '[data-skip-closure-task="' + sponsorTask . id + '"]' ) . selectOption ( 'not sponsored this week' ) ;
749766 await sponsorRow . locator ( '[data-save-completion-proof="' + sponsorTask . id + '"]' ) . click ( ) ;
750767 await expect ( page . locator ( '[data-task-row="' + sponsorTask . id + '"] [data-task-checkbox="' + sponsorTask . id + '"]' ) ) . toBeEnabled ( ) ;
@@ -784,6 +801,7 @@ test.describe('Bundle detail view (issue #27)', () => {
784801 await expect ( page . locator ( '.bundle-link-row' , { hasText : 'Mailchimp newsletter' } ) ) . not . toHaveClass ( / b u n d l e - l i n k - r o w - - e m p t y / ) ;
785802
786803 await expect ( page . locator ( '[data-task-row="' + externalStatusTask . id + '"]' ) ) . toContainText ( 'Add completion status: Mailchimp campaign scheduled' ) ;
804+ await expandRow ( externalStatusRow ) ;
787805 await externalStatusRow . locator ( '[data-completion-proof-task="' + externalStatusTask . id + '"]' ) . fill ( 'Mailchimp campaign scheduled' ) ;
788806 await externalStatusRow . locator ( '[data-save-completion-proof="' + externalStatusTask . id + '"]' ) . click ( ) ;
789807 await expect ( page . locator ( '[data-task-row="' + externalStatusTask . id + '"] [data-task-checkbox="' + externalStatusTask . id + '"]' ) ) . toBeEnabled ( ) ;
@@ -799,6 +817,7 @@ test.describe('Bundle detail view (issue #27)', () => {
799817 await expect ( commentRow ) . toContainText ( 'Add completion note: Book block updated or removed' ) ;
800818 await expect ( commentRow . locator ( '[data-task-checkbox="' + commentTask . id + '"]' ) ) . toBeDisabled ( ) ;
801819
820+ await expandRow ( commentRow ) ;
802821 await commentRow . locator ( '[data-completion-proof-task="' + commentTask . id + '"]' ) . fill ( 'Book block removed; no book this week' ) ;
803822 await commentRow . locator ( '[data-save-completion-proof="' + commentTask . id + '"]' ) . click ( ) ;
804823 await expect ( page . locator ( '[data-task-row="' + commentTask . id + '"] [data-task-checkbox="' + commentTask . id + '"]' ) ) . toBeEnabled ( ) ;
@@ -815,6 +834,7 @@ test.describe('Bundle detail view (issue #27)', () => {
815834 const skipRow = page . locator ( '[data-task-row="' + skipTask . id + '"]' ) ;
816835 await expect ( skipRow ) . toContainText ( 'Add LinkedIn link to complete' ) ;
817836 await expect ( skipRow . locator ( '[data-task-checkbox="' + skipTask . id + '"]' ) ) . toBeDisabled ( ) ;
837+ await expandRow ( skipRow ) ;
818838 await skipRow . locator ( '[data-skip-closure-task="' + skipTask . id + '"]' ) . selectOption ( 'not sponsored this week' ) ;
819839 await skipRow . locator ( '[data-save-completion-proof="' + skipTask . id + '"]' ) . click ( ) ;
820840 await expect ( page . locator ( '[data-task-row="' + skipTask . id + '"] [data-task-checkbox="' + skipTask . id + '"]' ) ) . toBeEnabled ( ) ;
@@ -1090,4 +1110,73 @@ test.describe('Bundle detail view (issue #27)', () => {
10901110 await expect ( regularRow ) . not . toHaveClass ( / m i l e s t o n e - t a s k - r o w / ) ;
10911111 } ) ;
10921112 } ) ;
1113+
1114+ // ── Scenario: Rows collapse to a scannable line and expand on demand ──
1115+ test . describe ( 'Scenario: Task rows collapse and expand (issue #102)' , ( ) => {
1116+ let bundleId ;
1117+ let taskId ;
1118+ const suffix = uid ( ) ;
1119+
1120+ test . beforeAll ( async ( { request } ) => {
1121+ const bundleRes = await request . post ( '/api/bundles' , {
1122+ data : {
1123+ title : 'Collapse ' + suffix ,
1124+ anchorDate : '2026-05-15' ,
1125+ bundleLinks : [ { name : 'Luma' , url : '' } ] ,
1126+ } ,
1127+ } ) ;
1128+ expect ( bundleRes . status ( ) ) . toBe ( 201 ) ;
1129+ bundleId = ( await bundleRes . json ( ) ) . bundle . id ;
1130+
1131+ const taskRes = await request . post ( '/api/tasks' , {
1132+ data : {
1133+ description : 'Collapsible required-link task ' + suffix ,
1134+ date : '2026-05-15' ,
1135+ bundleId,
1136+ requiredLinkName : 'Luma' ,
1137+ source : 'template' ,
1138+ } ,
1139+ } ) ;
1140+ expect ( taskRes . status ( ) ) . toBe ( 201 ) ;
1141+ taskId = ( await taskRes . json ( ) ) . id ;
1142+ } ) ;
1143+
1144+ test . afterAll ( async ( { request } ) => {
1145+ if ( taskId ) await request . delete ( '/api/tasks/' + taskId ) ;
1146+ if ( bundleId ) await archiveAndDelete ( request , bundleId ) ;
1147+ } ) ;
1148+
1149+ test ( 'row is collapsed at rest and reveals controls only after expand' , async ( { page } ) => {
1150+ await page . goto ( '/#/bundles' ) ;
1151+ await page . waitForSelector ( '.bundle-card' ) ;
1152+ await page . locator ( '.bundle-card' , { hasText : 'Collapse ' + suffix } ) . locator ( '.bundle-card-title' ) . click ( ) ;
1153+ await page . waitForSelector ( '[data-testid="stage-badge"]' ) ;
1154+
1155+ const row = page . locator ( '[data-task-row="' + taskId + '"]' ) ;
1156+ await expect ( row ) . toBeVisible ( ) ;
1157+
1158+ // At rest: the checkbox is visible but the detail editor is hidden.
1159+ await expect ( row . locator ( '.task-status-checkbox' ) ) . toBeVisible ( ) ;
1160+ const details = row . locator ( '.task-checklist-details' ) ;
1161+ await expect ( details ) . toBeHidden ( ) ;
1162+ await expect ( row . locator ( '.required-link-input' ) ) . toBeHidden ( ) ;
1163+
1164+ // The single next-action toggle exposes an accessible collapsed state.
1165+ const toggle = row . locator ( '[data-task-expand]' ) ;
1166+ await expect ( toggle ) . toBeVisible ( ) ;
1167+ await expect ( toggle ) . toHaveAttribute ( 'aria-expanded' , 'false' ) ;
1168+
1169+ // Keyboard-operable: focus and press Enter to expand.
1170+ await toggle . focus ( ) ;
1171+ await page . keyboard . press ( 'Enter' ) ;
1172+ await expect ( toggle ) . toHaveAttribute ( 'aria-expanded' , 'true' ) ;
1173+ await expect ( details ) . toBeVisible ( ) ;
1174+ await expect ( row . locator ( '.required-link-input' ) ) . toBeVisible ( ) ;
1175+
1176+ // Toggling again collapses the row back to one line.
1177+ await page . keyboard . press ( 'Enter' ) ;
1178+ await expect ( toggle ) . toHaveAttribute ( 'aria-expanded' , 'false' ) ;
1179+ await expect ( details ) . toBeHidden ( ) ;
1180+ } ) ;
1181+ } ) ;
10931182} ) ;
0 commit comments