auth-ui: rewrite ui

Change-Id: I6f00867015ec77aa7e336e89da4dc1b081e330c6
diff --git a/core/auth/ui/e2e/browser.go b/core/auth/ui/e2e/browser.go
index 19949f5..a21753a 100644
--- a/core/auth/ui/e2e/browser.go
+++ b/core/auth/ui/e2e/browser.go
@@ -99,6 +99,34 @@
 	Cleanup(func())
 }
 
+type browserSize struct {
+	Width  int `json:"width"`
+	Height int `json:"height"`
+}
+
+type browserSessionOptions struct {
+	Viewport      browserSize
+	VideoSize     browserSize
+	ReducedMotion bool
+}
+
+func defaultBrowserSessionOptions() browserSessionOptions {
+	return browserSessionOptions{
+		Viewport:  browserSize{Width: 1280, Height: 720},
+		VideoSize: browserSize{Width: 1280, Height: 720},
+	}
+}
+
+func validateBrowserSessionOptions(options browserSessionOptions) error {
+	if options.Viewport.Width <= 0 || options.Viewport.Height <= 0 {
+		return fmt.Errorf("browser viewport must have positive dimensions")
+	}
+	if options.VideoSize.Width <= 0 || options.VideoSize.Height <= 0 {
+		return fmt.Errorf("browser video must have positive dimensions")
+	}
+	return nil
+}
+
 type browserSession struct {
 	Page           playwright.Page
 	Context        playwright.BrowserContext
@@ -106,6 +134,8 @@
 	dir            string
 	video          playwright.Video
 	started        time.Time
+	viewport       browserSize
+	videoSize      browserSize
 	screenshots    []string
 	checkpoint     int
 	finalize       sync.Once
@@ -117,6 +147,8 @@
 	blocked        []string
 	requestsMu     sync.Mutex
 	requests       []requestMetadata
+	diagnosticsMu  sync.Mutex
+	diagnostics    []string
 	tracingStarted bool
 	screenshotOp   func(string) error
 	stopTraceOp    func(string) error
@@ -126,10 +158,18 @@
 }
 
 func newBrowserSession(t testReporter, browser playwright.Browser, root string, allowedOrigins []string) (*browserSession, error) {
+	return newBrowserSessionWithOptions(t, browser, root, allowedOrigins, defaultBrowserSessionOptions())
+}
+
+func newBrowserSessionWithOptions(t testReporter, browser playwright.Browser, root string, allowedOrigins []string, options browserSessionOptions) (*browserSession, error) {
+	if err := validateBrowserSessionOptions(options); err != nil {
+		return nil, err
+	}
 	session, err := newBrowserSessionOwner(t, root, browser.Version())
 	if err != nil {
 		return nil, err
 	}
+	assignBrowserSessionMetadata(session, options)
 	// Register while holding the construction/finalization lock. A watchdog
 	// either snapshots this ownership and waits here, or rejects construction
 	// before any external Playwright context exists.
@@ -144,11 +184,15 @@
 	}
 	defer session.lifecycleMu.Unlock()
 	videoDir := filepath.Join(session.dir, ".video")
-	context, err := browser.NewContext(playwright.BrowserNewContextOptions{
-		Viewport:       &playwright.Size{Width: 1280, Height: 720},
-		RecordVideo:    &playwright.RecordVideo{Dir: playwright.String(videoDir), Size: &playwright.Size{Width: 1280, Height: 720}},
+	contextOptions := playwright.BrowserNewContextOptions{
+		Viewport:       &playwright.Size{Width: options.Viewport.Width, Height: options.Viewport.Height},
+		RecordVideo:    &playwright.RecordVideo{Dir: playwright.String(videoDir), Size: &playwright.Size{Width: options.VideoSize.Width, Height: options.VideoSize.Height}},
 		ServiceWorkers: playwright.ServiceWorkerPolicyBlock,
-	})
+	}
+	if options.ReducedMotion {
+		contextOptions.ReducedMotion = playwright.ReducedMotionReduce
+	}
+	context, err := browser.NewContext(contextOptions)
 	if err != nil {
 		return nil, err
 	}
@@ -169,6 +213,14 @@
 	page.OnResponse(func(response playwright.Response) {
 		session.recordRequest(response.Request().Method(), response.Status(), response.Request().URL())
 	})
+	page.OnConsole(func(message playwright.ConsoleMessage) {
+		if message.Type() == "error" && !isExpectedFormStatusConsoleError(message.Text()) {
+			session.recordDiagnostic("console error: " + message.Text())
+		}
+	})
+	page.OnPageError(func(err error) {
+		session.recordDiagnostic("page error: " + err.Error())
+	})
 	s := session
 	s.Page = page
 	s.video = page.Video()
@@ -189,6 +241,11 @@
 	return s, nil
 }
 
+func assignBrowserSessionMetadata(session *browserSession, options browserSessionOptions) {
+	session.viewport = options.Viewport
+	session.videoSize = options.VideoSize
+}
+
 func newBrowserSessionOwner(t testReporter, root, browserVersion string) (*browserSession, error) {
 	dir := filepath.Join(root, sanitizeName(t.Name()))
 	if err := os.MkdirAll(filepath.Join(dir, "screenshots"), 0o700); err != nil {
@@ -197,7 +254,8 @@
 	if err := os.MkdirAll(filepath.Join(dir, ".video"), 0o700); err != nil {
 		return nil, err
 	}
-	session := &browserSession{t: t, dir: dir, started: time.Now().UTC(), browserVer: browserVersion}
+	defaults := defaultBrowserSessionOptions()
+	session := &browserSession{t: t, dir: dir, started: time.Now().UTC(), browserVer: browserVersion, viewport: defaults.Viewport, videoSize: defaults.VideoSize}
 	session.installDefaultArtifactOps()
 	// Own metadata and all available partial artifacts before Playwright context
 	// construction. If Playwright cannot create a context/page/trace, cleanup
@@ -275,6 +333,28 @@
 	return sortedStrings(s.blocked)
 }
 
+func isExpectedFormStatusConsoleError(message string) bool {
+	switch message {
+	case "Failed to load resource: the server responded with a status of 409 (Conflict)",
+		"Failed to load resource: the server responded with a status of 422 (Unprocessable Entity)":
+		return true
+	default:
+		return false
+	}
+}
+
+func (s *browserSession) recordDiagnostic(message string) {
+	s.diagnosticsMu.Lock()
+	s.diagnostics = append(s.diagnostics, message)
+	s.diagnosticsMu.Unlock()
+}
+
+func (s *browserSession) BrowserDiagnostics() []string {
+	s.diagnosticsMu.Lock()
+	defer s.diagnosticsMu.Unlock()
+	return append([]string(nil), s.diagnostics...)
+}
+
 func (s *browserSession) Checkpoint(name string) error {
 	s.lifecycleMu.Lock()
 	defer s.lifecycleMu.Unlock()
@@ -369,7 +449,7 @@
 		if s.t.Failed() || s.forcedFailure || len(errs) > 0 {
 			outcome = "failed"
 		}
-		metadata := sessionMetadata{TestName: s.t.Name(), StartedAt: s.started, FinishedAt: time.Now().UTC(), Outcome: outcome, BindingVersion: playwrightVersion, CLIVersion: playwrightCLIVersion, ChromiumRevision: chromiumRevision, BrowserVersion: s.browserVer, Screenshots: s.screenshots, FinalURL: finalURL}
+		metadata := sessionMetadata{TestName: s.t.Name(), StartedAt: s.started, FinishedAt: time.Now().UTC(), Outcome: outcome, BindingVersion: playwrightVersion, CLIVersion: playwrightCLIVersion, ChromiumRevision: chromiumRevision, BrowserVersion: s.browserVer, Viewport: s.viewport, VideoSize: s.videoSize, Screenshots: s.screenshots, FinalURL: finalURL}
 		data, err := json.MarshalIndent(metadata, "", "  ")
 		if err == nil {
 			err = os.WriteFile(filepath.Join(s.dir, "session.json"), append(data, '\n'), 0o600)
@@ -394,23 +474,19 @@
 	return "passed", false
 }
 
-const expectedExternalFontRequest = "https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css"
-
-func isExpectedBlockedBrowserRequest(request string) bool {
-	return request == expectedExternalFontRequest
-}
-
 type sessionMetadata struct {
-	TestName         string    `json:"test_name"`
-	StartedAt        time.Time `json:"started_at"`
-	FinishedAt       time.Time `json:"finished_at"`
-	Outcome          string    `json:"outcome"`
-	BindingVersion   string    `json:"binding_version"`
-	CLIVersion       string    `json:"playwright_cli_version"`
-	ChromiumRevision string    `json:"chromium_revision"`
-	BrowserVersion   string    `json:"browser_version"`
-	Screenshots      []string  `json:"screenshots"`
-	FinalURL         string    `json:"final_url"`
+	TestName         string      `json:"test_name"`
+	StartedAt        time.Time   `json:"started_at"`
+	FinishedAt       time.Time   `json:"finished_at"`
+	Outcome          string      `json:"outcome"`
+	BindingVersion   string      `json:"binding_version"`
+	CLIVersion       string      `json:"playwright_cli_version"`
+	ChromiumRevision string      `json:"chromium_revision"`
+	BrowserVersion   string      `json:"browser_version"`
+	Viewport         browserSize `json:"viewport"`
+	VideoSize        browserSize `json:"video_size"`
+	Screenshots      []string    `json:"screenshots"`
+	FinalURL         string      `json:"final_url"`
 }
 
 var unsafeName = regexp.MustCompile(`[^A-Za-z0-9._-]+`)