Component Lab B · Discovery patterns

Search and Lists Lab

Compare immediate, delayed, and debounced discovery behavior, then automate autocomplete, combined filters, load-more pagination, and a true cursor-backed infinite list.

PRACTICE FIXTURE — intentionally built for automation practice

Loading synthetic search data from the Practice API…

Instant, delayed, and debounced search

Three controls expose different timing contracts while returning the same synthetic employee shape.

Instant search

Type to filter immediately.

    Delayed API search

    Search has not run.

      Debounced search

      Waiting for input.

        Keyboard autocomplete

        Suggestions use combobox/listbox semantics, active descendant state, and deterministic keyboard selection.

          Type at least two characters.

          Combined filters and no-results state

          All selected filters apply together, and impossible combinations expose a stable empty result instead of a blank region.

          All employees shown.

            Load-more pagination

            Each button request appends one numbered API product page and preserves every previously visible result.

              No product page loaded.

              True API-backed infinite scroll

              The scroll boundary requests the next cursor page from the Product API until the server reports no next cursor.

                Preparing the first API page.

                Passing examples

                Start with observable behavior

                These snippets match executable tests stored inexamples/playwright-ts/.

                Assert a debounced search
                await page.getByLabel('Debounced list search').fill('Rahul');
                await expect(page.getByTestId('debounced-list-status')).toContainText('applied');
                await expect(page.locator('#debounced-list li')).toContainText(['Rahul']);
                Scroll until the API end state
                const list = page.getByTestId('infinite-list');
                for (;;) {
                  const end = page.getByText('End of API product list.');
                  if (await end.isVisible()) break;
                  await list.evaluate((node) => { node.scrollTop = node.scrollHeight; });
                  await expect(page.getByRole('status')).not.toContainText('Loading next');
                }