Wszystkie wpisy

Playwright + Copilot Chat - para przy E2E (kwiecień 2024)Playwright + Copilot Chat - para przy E2E (kwiecień 2024)

English

Playwright + Copilot Chat - para przy E2E (kwiecień 2024)

Kwiecień 2024: Copilot Chat GA przy specu Playwright 1.43. Szkic w sidebarze, zielone w runnerze. Bez Workspace i bez GPT-4o.

AutomatyzacjaPlaywright

Para w stabilnym VS Code, nie preview z maja

Jest 15 kwietnia i na moim ekranie są dwie rzeczy. Po lewej spec auth/login.spec.ts, po prawej sidebar z Copilot Chatem. Nic więcej nie jest mi potrzebne, żeby opisać, jak w tym miesiącu wygląda u mnie pair programming przy testach przeglądarkowych.

Warto zobaczyć, ile się tu zmieniło od ostatniego razu, kiedy pisałem o tym czacie. W maju zeszłego roku połowa wpisu była instrukcją wejścia: kanał Insiders, rozszerzenie Nightly, zapisy na listę i czekanie. Dziś otwieram ten sam stabilny edytor, w którym od czerwca 2022 mam szary tekst przed kursorem, klikam ikonę w pasku bocznym i czat po prostu jest. 29 grudnia wszedł w ogólną dostępność dla osób indywidualnych i dla organizacji, więc pierwszy raz piszę o nim bez akapitu o tym, jak go zdobyć.

Po drugiej stronie ekranu też jest pin i dla tego wpisu jest on ważniejszy. Runner to @playwright/test w wersji 1.43.1 z 12 kwietnia, czyli łatka na 1.43.0 z 4 kwietnia, zamykająca dwie regresje w trybie UI. Podaję numer z dokładnością do trzeciej cyfry, bo cała druga połowa tego wpisu jest o tym, że czat nie wie, którą wersję mam zainstalowaną, a ja wiedzieć muszę.

Moja teza na kwiecień 2024 jest wąska i będę jej bronił przez cały tekst. Para przy E2E to Copilot Chat w ogólnej dostępności obok otwartego speca na Playwright 1.43: czat szkicuje krok i asercję, ja odpalam npx playwright test i odrzucam zmyślony data-testid. Tyle. To nie jest drugie wprowadzenie do czatu, bo wprowadzenie było w maju. Nie jest to też most od zgłoszenia przez plan do pull requesta, zapowiedziany na listopadowym Universe jako Copilot Workspace: tej rzeczy nie mam w rękach, nie mam do niej linku i nie zamierzam opisywać cudzych slajdów. Miesiąc temu rozplątywałem w notatce kolizję nazw między tamtym produktem a agentem @workspace w oknie czatu. Dziś piszę o czymś mniejszym i dużo bardziej codziennym: o tym, co dzieje się między sidebarem a runnerem.

Ownerem asercji zostaję ja i nie jest to deklaracja światopoglądowa, tylko opis tego, kto w tej parze ma dostęp do dowodu. Czat widzi plik, który mam otwarty. Nie widzi DOM uruchomionej aplikacji, nie widzi zapisu przebiegu i nie wie, czy komunikat o błędzie w ogóle dojechał do przeglądarki. Recenzuję więc jego szkic dokładnie tak, jak od kwietnia zeszłego roku recenzuję pojedynczą podpowiedź w linii, tylko obiekt recenzji jest nieco większy: nie jedna linia, ale krok plus asercja.

Stan warsztatu na dziś, żeby za rok było wiadomo, o czym mówię. Runner 1.43.1. Czat w stabilnym VS Code od 29 grudnia, na tej samej subskrypcji, którą płacę za szary tekst. Agent @workspace, który przeszukuje repozytorium, jest w changelogu GitHuba z 30 listopada i wołam go wtedy, kiedy helper leży w innym pliku niż spec. Component testing jest nadal experimental i nie jest tematem tej pary; pisałem o nim w sierpniu. Claude 3 jest od 4 marca realnym konkurentem, tylko dla karty w przeglądarce, a nie dla pary w edytorze. Model stojący za czatem zmienia się zresztą bez mojego udziału: 9 kwietnia pojawił się świeższy snapshot GPT-4 Turbo i dla tego wpisu nie wynika z tego nic, bo jedyną wersją, która cokolwiek tu rozstrzyga, jest wersja runnera. Edytor Cursor nadal nie jest moim codziennym narzędziem.

Czego tu nie będzie. Nie będzie zdania, że czat siedzi za waitlistą, bo od grudnia nie siedzi. Nie będzie tutoriala agenta, który sam dopina patcha do zgłoszenia. Nie będzie inita projektu ani sporu o wybór narzędzia, bo pierwszy projekt Playwright i decyzję Playwright zamiast Cypressa mam za sobą od 2022 roku. Będzie syntetyczne logowanie z repozytorium z przykładami do bloga, jedno kryterium, pięć szkiców z sidebara i trzy skreślenia.

Co wkładam w kontekst pary

Para zaczyna się od tego, co czat ma przed sobą, i jest to jedyna część tej pracy, nad którą mam pełną kontrolę. Otwieram trzy rzeczy i żadna z nich nie pochodzi z .env.

Pierwsza to spec. Happy path już w nim jest:

import { expect, test } from '@playwright/test'
import { LoginPage } from '../pages/LoginPage'

test('logowanie poprawnym hasłem wpuszcza na pulpit', async ({ page }) => {
  const login = new LoginPage(page)

  await login.open()
  await login.signIn('uzytkownik@example.test', 'poprawne-haslo')

  await expect(page.getByRole('heading', { name: 'Pulpit' })).toBeVisible()
})

Druga to page object pages/LoginPage.ts, z lokatorami w klasie strony, w konwencji, którą ustawiłem przy refaktorze POM. Ma pola email, password i submit oraz metodę signIn. Nie ma niczego, czym dałoby się przeczytać komunikat o błędzie, i to jest dokładnie ta dziura, którą w tym tygodniu zamykam.

Trzecia to kryterium, wklejone do czatu jako jedno zdanie z mojej notatki: złe hasło zostawia użytkownika na formularzu i pokazuje alert o treści “Nieprawidłowe hasło”.

Kiedy czat widzi sam spec, zgaduje page object. Zgaduje przy tym zaskakująco dobrze, bo nazwa LoginPage w imporcie mówi mu wiele, i zgaduje źle w jednym szczególe, który zawsze kosztuje najwięcej: dopisuje metody, których w tej klasie nie ma. Dlatego przy pytaniu o drugi scenariusz albo wołam @workspace, żeby czat sam dociągnął plik klasy, albo wklejam samą sygnaturę, kiedy chcę węższy kontekst i krótszą odpowiedź. Obie drogi mają tę samą granicę i warto ją powiedzieć wprost: @workspace indeksuje repozytorium, a nie drzewo DOM uruchomionej strony.

Czego nie wkładam: connection stringów, tokenów do środowiska testowego i prawdziwych haseł kont serwisowych. Logowanie w tym demo jest syntetyczne, a użytkownik nazywa się uzytkownik@example.test, bo para, która do działania potrzebuje sekretu wklejonego w okno czatu, nie jest parą, której używam.

Pytanie, od którego zacząłem ten tydzień, zmieściło się w czterech linijkach:

W auth/login.spec.ts mam tylko happy path. Dopisz scenariusz złego hasła.
Komunikat ma rolę alert i treść "Nieprawidłowe hasło".
Nie używaj waitForTimeout. Jeśli brakuje lokatora, dopisz go w pages/LoginPage.ts.
Pokaż sam diff, niczego nie zapisuj.

Ostatnia linijka nie jest tam przypadkiem. Odpowiedź ma przyjść jako tekst w sidebarze, a nie jako zmiana w pliku, bo pierwszym krokiem tej pętli jest czytanie, nie akceptacja.

Pętla pary ma cztery kroki

Żaden z tych kroków nie nazywa się “zastosuj i zapomnij” i jest to jedyna rzecz, którą z całej sekcji warto zapamiętać.

Krok pierwszy: czytam szkic. Czat proponuje lokator i expect, zwykle w kilkanaście sekund i zwykle w kształcie, który wygląda jak mój własny kod, bo widział sąsiedni test. Traktuję to jak komentarz kolegi pod pull requestem, a nie jak kod, który jest już mój. Różnica siedzi w domyślnej odpowiedzi: pod cudzym komentarzem domyślną odpowiedzią jest “sprawdzę”, a pod przyciskiem “apply” domyślną odpowiedzią jest “tak”.

Krok drugi: odpalam test. Zaakceptowany fragment ląduje w specu i od razu idzie npx playwright test auth/login.spec.ts. Auto-waiting Playwrighta zostaje nietknięty. Nie proszę czatu o waitForTimeout i nie przyjmuję go, kiedy pojawi się sam. Sleep, który “naprawia” wyścig, nie jest zły dlatego, że nie działa. Jest zły dlatego, że kiedy przypadkiem zadziała, nikt już nigdy nie zapyta, na co właściwie czekaliśmy.

Krok trzeci: czytam zapis przebiegu. Przy czerwonym wyniku otwieram trace. Na 1.43 mam w konfiguracji tryb, który doszedł w tej właśnie wersji:

import { defineConfig } from '@playwright/test'

export default defineConfig({
  use: {
    trace: 'retain-on-first-failure',
  },
})

Nagrywa on ślad przy pierwszym uruchomieniu każdego testu i nie nagrywa go przy ponowieniach, a potem zostawia plik, kiedy test ostatecznie pada, i kasuje, kiedy przechodzi. Dla pary jest to dobry domyślny wybór, bo interesuje mnie ten przebieg, w którym szkic czatu padł po raz pierwszy, a nie trzecia próba po retry. W trace szukam trzech rzeczy: czy komunikat w ogóle trafił do DOM, czy trafił, ale był ukryty, czy trafił do kontenera z aria-live zamiast do elementu z rolą alert. Czat nie widzi żadnej z nich. Widzi plik w stanie, w jakim był, kiedy pytałem.

Krok czwarty: wracam z błędem, nie z prośbą. Do sidebara wracam z konkretem: wklejam komunikat runnera i dopisuję jedno zdanie o tym, czego nie robić.

Padło: getByTestId('login-error') nie znajduje elementu.
Tego atrybutu nie ma w aplikacji. Użyj roli alert.

Nie piszę “napraw to jakoś”, bo prośba bez treści zwraca szkic bez treści: model zamieni jeden zmyślony lokator na drugi, równie zmyślony, i zrobi to z tą samą pewnością siebie co za pierwszym razem. Im ciaśniejszy powrót, tym mniej miejsca zostaje na kolejny wymysł. Para ma u mnie budżet dwóch powrotów. Przy trzecim zamykam okno i dopisuję asercję sam, bo w tym momencie czytanie następnych szkiców kosztuje więcej niż napisanie dwóch linii.

Pętla kończy się więc na runnerze i na trace, a jedno i drugie należy do mnie. To jest cała różnica między parą a agentem z własną pętlą narzędzi: agent sam zamyka cel, para oddaje tekst i czeka na werdykt kogoś, kto ma dostęp do dowodu.

Gdzie Chat kłamie przy E2E

Pięć szkiców na jedno kryterium, dwa wklejone, trzy skreślone. Skreślenia rozpisuję osobno, bo każde z nich jest inną kategorią błędu i, co ważniejsze, każde łapie się w innym miejscu procesu.

Skreślenie pierwsze: data-testid, którego nie ma. Pierwszy szkic wyglądał tak:

await login.signIn('uzytkownik@example.test', 'zle-haslo')
await expect(page.getByTestId('login-error')).toBeVisible()

Atrybutu login-error nie ma w komponencie formularza, nie ma go w page objectcie i nie ma go w drzewie, które widzę w trace. Model dopisał go, bo w publicznym kodzie komunikaty błędu bardzo często mają stabilny atrybut testowy, i jako zgadywanka jest to zgadywanka statystycznie dobra. Runner odpowiada na nią tak:

  1) [chromium] › auth/login.spec.ts:14:3 › logowanie złym hasłem pokazuje komunikat

    Error: Timed out 5000ms waiting for expect(locator).toBeVisible()

    Locator:  getByTestId('login-error')
    Expected: visible
    Received: <element(s) not found>

Tu jest prawdziwy koszt tej klasy błędu i nie leży on w straconych pięciu sekundach. Ten wynik nie wygląda na zmyślenie. Wygląda na flaka środowiska. Test czeka na element, który nigdy nie powstanie, pada po timeoucie i ląduje w raporcie obok prawdziwych timeoutów sieciowych, czyli dokładnie w tym miejscu, w które zespół nauczył się nie patrzeć uważnie. Asercja, która przechodzi, jest krótsza i nie ma w sobie niczego wymyślonego:

await expect(login.error).toHaveText('Nieprawidłowe hasło')

login.error to getByRole('alert') dopisany do page objectu, bo formularz wypisuje komunikat do kontenera z tą rolą. Rola jest w HTML-u aplikacji. Testid był w wyobraźni modelu.

Skreślenie drugie: asercja, której nie ma na moim pinie. Drugi szkic skracał to samo sprawdzenie przez asercję dostępnościową, a w tej samej odpowiedzi czat dorzucił do niej jeszcze sprawdzenie roli:

await expect(login.error).toHaveAccessibleName('Nieprawidłowe hasło')

Na 1.43.1 nie mam ani jednej, ani drugiej z tych asercji, więc odpowiedź przychodzi z kompilatora, zanim w ogóle wstanie przeglądarka:

Property 'toHaveAccessibleName' does not exist on type 'LocatorAssertions'.

To najtańsze z trzech skreśleń i najbardziej pouczające, bo pokazuje, czym naprawdę jest “wiedza” modelu o Playwrighcie. Jest rozkładem prawdopodobieństwa po dokumentacji wielu wersji, a nie odczytem z mojego package.json. Mój projekt ma jedną wersję, tę z pliku lock, i to runner, a nie okno czatu, jest źródłem prawdy o tym, co w API istnieje. W tej jednej sprawie TypeScript bywa szybszym recenzentem ode mnie.

Skreślenie trzecie: asercja, która przechodzi zawsze. Trzeci szkic zamiast komunikatu o błędzie asertował nagłówek z góry strony logowania:

await expect(page.getByText('Witaj z powrotem')).toBeVisible()

Ten tekst jest na stronie przed wpisaniem czegokolwiek i zostaje na niej po wpisaniu złego hasła, więc asercja świeci na zielono niezależnie od tego, co robi logowanie. To nie jest test błędnego hasła. To jest test tego, że strona się wyrenderowała.

Te trzy skreślenia różnią się tym, kto je łapie, i ta różnica jest dla mnie ważniejsza niż ich treść. Pierwsze łapie runner, kosztem jednego timeoutu. Drugie łapie kompilator, kosztem sekundy. Trzeciego nie złapie nic poza człowiekiem w recenzji, bo jest zielone od pierwszego uruchomienia i zostanie zielone na zawsze, także wtedy, kiedy formularz przestanie pokazywać jakikolwiek komunikat. Dlatego recenzja zostaje moja i dlatego jest to ta sama praca, którą opisywałem rok temu przy podpowiedziach w linii: zielony wynik nigdy nie był dowodem na to, że asercja ma sens.

Czego nie deleguję

Czat przyspiesza pierwszy szkic speca i nie udaję, że to mało. Drugi scenariusz logowania powstaje w sidebarze szybciej, niż sam wyklepię import, konstruktor page objectu i trzy linie boilerplate’u. Nie przyspiesza natomiast żadnej z decyzji, które sprawiają, że ten zestaw testów jest cokolwiek wart.

Pierwsza decyzja: czy ten scenariusz w ogóle zasługuje na E2E. Złe hasło na formularzu logowania jest u mnie bramką w pull requeście, a nie częścią nocnego przebiegu całej aplikacji, i tę granicę rozpisałem we wrześniu 2022. Czy to samo zachowanie ma dodatkowo żyć jako test jednostkowy na reduktorze komunikatu, to pytanie do piramidy, nie do sidebara. Czat napisze mi oba warianty z przyjemnością i w żadnym miejscu nie powie, że jeden z nich wystarczy.

Druga: co zostaje poza parą. Component testing jest nadal experimental i nie wchodzi do tego speca. Nie mieszam montażu komponentu z logowaniem end-to-end tylko dlatego, że czat zna oba API i chętnie pokaże oba; o tym, gdzie przebiega ta granica, pisałem w sierpniu i od tamtej pory nie zmieniłem zdania.

Trzecia: co odnotowuję, a czego nie wprowadzam do warsztatu. SWE-agent, ogłoszony 2 kwietnia, odnotowuję jednym zdaniem jako research o drodze od zgłoszenia do patcha, a nie jako narzędzie, które stawiam obok speca. Claude 3 zostaje w karcie przeglądarki: żeby odpowiedział na pytanie z drugiej sekcji, musiałbym wkleić mu spec, page object i notatkę, a potem przepisać odpowiedź z powrotem do plików, czyli wrócić do przełączania kontekstu, które czat w edytorze właśnie mi zdjął. Parą przy E2E jest Copilot nie dlatego, że jest mądrzejszy, tylko dlatego, że widzi plik, który mam otwarty.

Podsumowanie

W kwietniu 2024 para przy E2E to Copilot Chat w ogólnej dostępności obok otwartego speca na Playwright 1.43. Szkic powstaje w sidebarze. Zielone wydaje runner. Pod mergem podpisuje się człowiek.

Trzy zdania, które chcę móc przeczytać za rok. Czat w stabilnym edytorze szkicuje drugi scenariusz logowania szybciej, niż sam piszę boilerplate, i jest to realny zysk wobec maja zeszłego roku, kiedy to samo narzędzie trzeba było najpierw zdobyć. Z pięciu szkiców na jedno kryterium wkleiłem dwa, a trzy skreśliłem: testid, którego nie ma w aplikacji, asercję, której nie ma w 1.43.1, i zielone sprawdzenie nagłówka, który jest na stronie zawsze. Pętla kończy się odpaleniem testu i czytaniem trace, a nie przyciskiem “apply”.

Czego z tego nie wynika. Nie wynika, że wracam do Insidersa, bo ogólna dostępność z 29 grudnia dała dokładnie to, czego w maju brakowało: ten sam edytor, co reszta roboty. Nie wynika, że czat może sam uznać spec za zielony, bo z trzech skreśleń tylko dwa złapało narzędzie, a trzecie złapał człowiek. Nie wynika też, że most od zgłoszenia przez plan do pull requesta już u mnie działa, bo go nie mam i nie wiem, kiedy będę miał.

Warunek, po którym poznam, że ta para dojrzała, formułuję tak: dwa szkice pod rząd na tym samym kryterium użyją roli, która naprawdę jest w DOM, i API, które naprawdę jest w 1.43.1, a jedyną niespodzianką w runnerze będzie zachowanie aplikacji, nie zawartość szkicu. Do tego czasu szkicuję z czatem i merguję sam.