@@ -7,6 +7,7 @@ import attachments from '../../../acceptance/attachments/attachments.js'
77import examplesTables from '../../../acceptance/examples-tables/examples-tables.js'
88import hooksConditional from '../../../acceptance/hooks-conditional/hooks-conditional.js'
99import retry from '../../../acceptance/retry/retry.js'
10+ import rules from '../../../acceptance/rules/rules.js'
1011import randomOrderRun from '../../../samples/random-order-run.js'
1112import targetedRun from '../../../samples/targeted-run.js'
1213import { EnvelopesProvider } from './EnvelopesProvider.js'
@@ -94,8 +95,130 @@ describe('FilteredDocuments', () => {
9495 } )
9596 } )
9697
98+ describe ( 'filtering by tag expression' , ( ) => {
99+ it ( 'shows no results based on a tag expression that doesnt match anything' , async ( ) => {
100+ const { getByText } = render (
101+ < EnvelopesProvider envelopes = { hooksConditional } >
102+ < InMemorySearchProvider defaultQuery = "@nonexistent" >
103+ < FilteredDocuments />
104+ </ InMemorySearchProvider >
105+ </ EnvelopesProvider >
106+ )
107+
108+ await waitFor ( ( ) => {
109+ expect ( getByText ( 'No scenarios match your query and/or filters.' ) ) . to . be . visible
110+ } )
111+ } )
112+
113+ it ( 'matches based on a single tag' , async ( ) => {
114+ const { getByRole, queryByRole } = render (
115+ < EnvelopesProvider envelopes = { hooksConditional } >
116+ < InMemorySearchProvider defaultQuery = "@fail-before" >
117+ < FilteredDocuments />
118+ </ InMemorySearchProvider >
119+ </ EnvelopesProvider >
120+ )
121+
122+ await waitFor ( ( ) =>
123+ getByRole ( 'heading' , {
124+ name : 'Scenario: A failure in the before hook and a skipped step' ,
125+ } )
126+ )
127+
128+ expect (
129+ queryByRole ( 'heading' , { name : 'Scenario: A failure in the after hook and a passed step' } )
130+ ) . not . to . exist
131+ expect ( queryByRole ( 'heading' , { name : 'Scenario: With an tag, a passed step and hook' } ) ) . not
132+ . to . exist
133+ } )
134+
135+ it ( 'matches based on an or expression' , async ( ) => {
136+ const { getByRole, queryByRole } = render (
137+ < EnvelopesProvider envelopes = { hooksConditional } >
138+ < InMemorySearchProvider defaultQuery = "@fail-before or @fail-after" >
139+ < FilteredDocuments />
140+ </ InMemorySearchProvider >
141+ </ EnvelopesProvider >
142+ )
143+
144+ await waitFor ( ( ) =>
145+ getByRole ( 'heading' , {
146+ name : 'Scenario: A failure in the before hook and a skipped step' ,
147+ } )
148+ )
149+
150+ expect (
151+ getByRole ( 'heading' , { name : 'Scenario: A failure in the after hook and a passed step' } )
152+ ) . to . be . visible
153+ expect ( queryByRole ( 'heading' , { name : 'Scenario: With an tag, a passed step and hook' } ) ) . not
154+ . to . exist
155+ } )
156+
157+ it ( 'matches based on a negated expression' , async ( ) => {
158+ const { getByRole, queryByRole } = render (
159+ < EnvelopesProvider envelopes = { hooksConditional } >
160+ < InMemorySearchProvider defaultQuery = "not @passing-hook" >
161+ < FilteredDocuments />
162+ </ InMemorySearchProvider >
163+ </ EnvelopesProvider >
164+ )
165+
166+ await waitFor ( ( ) =>
167+ getByRole ( 'heading' , {
168+ name : 'Scenario: A failure in the before hook and a skipped step' ,
169+ } )
170+ )
171+
172+ expect (
173+ getByRole ( 'heading' , { name : 'Scenario: A failure in the after hook and a passed step' } )
174+ ) . to . be . visible
175+ expect ( queryByRole ( 'heading' , { name : 'Scenario: With an tag, a passed step and hook' } ) ) . not
176+ . to . exist
177+ } )
178+
179+ it ( 'matches based on tags inherited from rule' , async ( ) => {
180+ const { getByRole, queryByRole } = render (
181+ < EnvelopesProvider envelopes = { rules } >
182+ < InMemorySearchProvider defaultQuery = "@some-tag" >
183+ < FilteredDocuments />
184+ </ InMemorySearchProvider >
185+ </ EnvelopesProvider >
186+ )
187+
188+ await waitFor ( ( ) => getByRole ( 'button' , { name : 'samples/rules/rules.feature' } ) )
189+ await userEvent . click ( getByRole ( 'button' , { name : 'samples/rules/rules.feature' } ) )
190+
191+ await waitFor ( ( ) => getByRole ( 'heading' , { name : 'Example: No chocolates left' } ) )
192+
193+ // scenario inside rule with @some -tag is shown
194+ expect ( getByRole ( 'heading' , { name : 'Example: No chocolates left' } ) ) . to . be . visible
195+ // scenarios inside rule without @some -tag are excluded
196+ expect ( queryByRole ( 'heading' , { name : 'Example: Not enough money' } ) ) . not . to . exist
197+ expect ( queryByRole ( 'heading' , { name : 'Example: Enough money' } ) ) . not . to . exist
198+ } )
199+
200+ it ( 'matches based on tags inherited from examples table' , async ( ) => {
201+ const { getByRole, queryByRole } = render (
202+ < EnvelopesProvider envelopes = { examplesTables } >
203+ < InMemorySearchProvider defaultQuery = "@passing" >
204+ < FilteredDocuments />
205+ </ InMemorySearchProvider >
206+ </ EnvelopesProvider >
207+ )
208+
209+ await waitFor ( ( ) => getByRole ( 'heading' , { name : 'Examples: These are passing' } ) )
210+
211+ // examples with @passing tag are shown
212+ expect ( getByRole ( 'heading' , { name : 'Then I should have 7 cucumbers' } ) ) . to . be . visible
213+ expect ( getByRole ( 'heading' , { name : 'Then I should have 15 cucumbers' } ) ) . to . be . visible
214+ // examples with @failing tag (not matching) should not be visible
215+ expect ( queryByRole ( 'heading' , { name : 'When I eat 20 cucumbers' } ) ) . not . to . exist
216+ expect ( queryByRole ( 'heading' , { name : 'When I eat 1 cucumbers' } ) ) . not . to . exist
217+ } )
218+ } )
219+
97220 describe ( 'filtering by status' , ( ) => {
98- it ( 'should show a message if we filter all statuses out ' , async ( ) => {
221+ it ( 'shows no results when all statuses are hidden ' , async ( ) => {
99222 const { queryByRole, getByText } = render (
100223 < EnvelopesProvider envelopes = { examplesTables } >
101224 < InMemorySearchProvider
@@ -120,7 +243,7 @@ describe('FilteredDocuments', () => {
120243 } )
121244 } )
122245
123- it ( 'shows only passed scenarios when other statuses are hidden ' , async ( ) => {
246+ it ( 'shows only passed scenarios' , async ( ) => {
124247 const { getByRole, getByText, queryByText } = render (
125248 < EnvelopesProvider envelopes = { retry } >
126249 < InMemorySearchProvider
@@ -150,7 +273,7 @@ describe('FilteredDocuments', () => {
150273 . exist
151274 } )
152275
153- it ( 'shows only failed scenarios when other statuses are hidden ' , async ( ) => {
276+ it ( 'shows only failed scenarios' , async ( ) => {
154277 const { getByRole, queryByRole, getByText } = render (
155278 < EnvelopesProvider envelopes = { retry } >
156279 < InMemorySearchProvider
@@ -188,7 +311,7 @@ describe('FilteredDocuments', () => {
188311 ) . not . to . exist
189312 } )
190313
191- it ( 'shows scenarios matching any of multiple statuses' , async ( ) => {
314+ it ( 'shows scenarios matching multiple statuses' , async ( ) => {
192315 const { getByRole, getByText } = render (
193316 < EnvelopesProvider envelopes = { retry } >
194317 < InMemorySearchProvider
@@ -217,6 +340,25 @@ describe('FilteredDocuments', () => {
217340 . visible
218341 } )
219342
343+ it ( 'shows only matching examples within a scenario outline' , async ( ) => {
344+ const { getByRole, queryByRole } = render (
345+ < EnvelopesProvider envelopes = { examplesTables } >
346+ < InMemorySearchProvider defaultHideStatuses = { [ TestStepResultStatus . FAILED ] } >
347+ < FilteredDocuments />
348+ </ InMemorySearchProvider >
349+ </ EnvelopesProvider >
350+ )
351+
352+ await waitFor ( ( ) => getByRole ( 'heading' , { name : 'Scenario Outline: Eating cucumbers' } ) )
353+
354+ // passing examples should be visible
355+ expect ( getByRole ( 'heading' , { name : 'Then I should have 7 cucumbers' } ) ) . to . be . visible
356+ expect ( getByRole ( 'heading' , { name : 'Then I should have 15 cucumbers' } ) ) . to . be . visible
357+ // failing examples should not be visible
358+ expect ( queryByRole ( 'heading' , { name : 'When I eat 20 cucumbers' } ) ) . not . to . exist
359+ expect ( queryByRole ( 'heading' , { name : 'When I eat 1 cucumbers' } ) ) . not . to . exist
360+ } )
361+
220362 it ( 'treats scenarios with failed before hooks as failed' , async ( ) => {
221363 const { getByRole, getByText, queryByText } = render (
222364 < EnvelopesProvider envelopes = { hooksConditional } >
@@ -277,4 +419,35 @@ describe('FilteredDocuments', () => {
277419 expect ( queryByText ( 'With an tag, a passed step and hook' ) ) . not . to . exist
278420 } )
279421 } )
422+
423+ describe ( 'searching and filtering together' , ( ) => {
424+ it ( 'narrows results with both text search and status filter' , async ( ) => {
425+ const { getByRole, getByText, queryByText } = render (
426+ < EnvelopesProvider envelopes = { retry } >
427+ < InMemorySearchProvider
428+ defaultQuery = "always"
429+ defaultHideStatuses = { [ TestStepResultStatus . FAILED ] }
430+ >
431+ < FilteredDocuments />
432+ </ InMemorySearchProvider >
433+ </ EnvelopesProvider >
434+ )
435+
436+ await waitFor ( ( ) => getByRole ( 'button' , { name : 'samples/retry/retry.feature' } ) )
437+ await userEvent . click ( getByRole ( 'button' , { name : 'samples/retry/retry.feature' } ) )
438+
439+ await waitFor ( ( ) => getByText ( "Test cases that pass aren't retried" ) )
440+
441+ // scenario 1 matches both filters (step has "always", status is PASSED)
442+ expect ( getByText ( "Test cases that pass aren't retried" ) ) . to . be . visible
443+ // scenarios 2 and 3 don't match text search (no "always" in steps)
444+ expect ( queryByText ( 'Test cases that fail are retried if within the --retry limit' ) ) . not . to
445+ . exist
446+ expect ( queryByText ( 'Test cases that fail will continue to retry up to the --retry limit' ) ) . not
447+ . to . exist
448+ // scenario 4 matches text search but not status filter (it's FAILED)
449+ expect ( queryByText ( "Test cases won't retry after failing more than the --retry limit" ) ) . not . to
450+ . exist
451+ } )
452+ } )
280453} )
0 commit comments