Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
7926919
ci(embedded): add Patrol E2E suite with local mock auth and matrix sh…
dianaKhortiuk-frontegg Mar 30, 2026
36685f6
fix(e2e): resolve analyzer errors, patrol_cli compat, and lint warnings
dianaKhortiuk-frontegg Mar 30, 2026
60a56b7
fix(e2e): rename test file to _test.dart and remove last redundant args
dianaKhortiuk-frontegg Mar 30, 2026
651a091
fix(e2e): bump patrol constraint to ^3.14.0 for patrol_cli 3.6 compat
dianaKhortiuk-frontegg Mar 30, 2026
adcd17e
ci(e2e): pick available iPhone Simulator by UDID on iOS job
dianaKhortiuk-frontegg Mar 30, 2026
40122e3
fix(ios): wire RunnerUITests to Pods-Runner-RunnerUITests xcconfigs
dianaKhortiuk-frontegg Mar 30, 2026
51b4aeb
fix(e2e): Android SDK init without unreleased API; disable Jetifier; …
dianaKhortiuk-frontegg Mar 30, 2026
cd9497d
fix(ios): read enableOfflineMode from Flutter as NSNumber when needed
dianaKhortiuk-frontegg Mar 30, 2026
48c9f5d
ci(ios): clear DEVELOPMENT_TEAM on GHA; add Flutter Profile.xcconfig …
dianaKhortiuk-frontegg Mar 30, 2026
8c04871
ci(ios): align embedded E2E with Swift demo (team, simulator, logs)
dianaKhortiuk-frontegg Mar 30, 2026
0d4e634
fix(ci): shard iOS E2E with E2E_TEST_FILTER and honor failures
dianaKhortiuk-frontegg Mar 30, 2026
a3c0699
fix(ci): one patrol run per E2E shard (comma-separated filter)
dianaKhortiuk-frontegg Mar 30, 2026
1bd70c1
fix(ios): resolve FronteggSwift via CocoaPods for CI builds
dianaKhortiuk-frontegg Mar 30, 2026
47751d1
fix(ios): switch to SPM for FronteggSwift dependency resolution
dianaKhortiuk-frontegg Mar 30, 2026
d283941
fix(e2e): replace pumpAndSettle with pump to avoid SDK timer timeout
dianaKhortiuk-frontegg Mar 30, 2026
b857a3a
fix(e2e): allow cleartext HTTP to mock server on both platforms
dianaKhortiuk-frontegg Mar 31, 2026
04cce45
fix(e2e): fix lint error, PatrolLogReader crash, and improve diagnostics
dianaKhortiuk-frontegg Mar 31, 2026
c72b1cd
diag: add byType/state diagnostics, explicit frame pump in launchApp
dianaKhortiuk-frontegg Mar 31, 2026
5a5857e
fix: replace bySemanticsLabel with byWidgetPredicate for test finders
dianaKhortiuk-frontegg Mar 31, 2026
33d8d49
fix: scroll to off-screen E2E buttons before tapping
dianaKhortiuk-frontegg Mar 31, 2026
e307a28
fix: tapWebButtonIfPresent no-op on missing button + fix remaining by…
dianaKhortiuk-frontegg Mar 31, 2026
b6beef8
fix: align loginWithPassword with Swift reference + upgrade patrol_log
dianaKhortiuk-frontegg Mar 31, 2026
22dd0c3
fix: replace pump(Duration) with Future.delayed to prevent hang
dianaKhortiuk-frontegg Mar 31, 2026
698f3d4
ci: cut runner costs, harden Android E2E, retry Patrol
dianaKhortiuk-frontegg Mar 31, 2026
e703676
fix(e2e): restore bare pump() after delays for launch and waits
dianaKhortiuk-frontegg Mar 31, 2026
d642ec6
fix(e2e): avoid pump() during webview waits + extend CI job timeout
dianaKhortiuk-frontegg Mar 31, 2026
b8cb02c
ci(e2e): smaller shards (2 tests) + disable Patrol retry doubling
dianaKhortiuk-frontegg Apr 1, 2026
f2e12e0
fix(e2e): remove pump from waitForSemantics, 1 test/shard, 120m CI
dianaKhortiuk-frontegg Apr 2, 2026
5604a34
fix(e2e): selective pump for UserPageRoot vs in-app auth flows
dianaKhortiuk-frontegg Apr 2, 2026
f7f7325
fix(embedded-e2e): offline semantics parity, harness timeouts, skip A…
dianaKhortiuk-frontegg Apr 2, 2026
426ef57
fix(e2e): avoid pump deadlock on email wait; offline state + No Conne…
dianaKhortiuk-frontegg Apr 2, 2026
d55e735
fix(embedded-e2e): gate NoConnection on loading; pump after UserPageR…
dianaKhortiuk-frontegg Apr 2, 2026
5fdeefd
ci(e2e): stop duplicate Android checks from JUnit reporter
dianaKhortiuk-frontegg Apr 2, 2026
5ff5572
fix(e2e): cancel duplicate runs, dedupe build check, fix mock auth re…
dianaKhortiuk-frontegg Apr 5, 2026
a72843c
fix(ci): reduce checks from 47 to ~11, fix lint, fix emulator boot
dianaKhortiuk-frontegg Apr 5, 2026
b16e338
fix(ci): enable KVM for Android emulator, drop custom memory flags
dianaKhortiuk-frontegg Apr 5, 2026
7f66d2a
fix(ci): exclude testRequestAuthorizeFlow from Android E2E matrix
dianaKhortiuk-frontegg Apr 5, 2026
980915e
fix(e2e): use mock.frontegg.local to bypass SDK localhost webview block
dianaKhortiuk-frontegg Apr 5, 2026
e6c208c
fix(e2e): use mock.frontegg.local only on iOS, keep 127.0.0.1 for And…
dianaKhortiuk-frontegg Apr 5, 2026
501a328
fix(e2e): use .test TLD instead of .local (macOS reserves .local for …
dianaKhortiuk-frontegg Apr 5, 2026
e2d897b
fix(e2e): add diagnostics to waitForSemantics timeout, reduce iOS matrix
dianaKhortiuk-frontegg Apr 5, 2026
101b808
fix(e2e): add os_log diagnostics to initializeForE2E and e2e channel …
dianaKhortiuk-frontegg Apr 5, 2026
ce650e6
fix(e2e): use FlutterPluginRegistry to register e2e channel
dianaKhortiuk-frontegg Apr 6, 2026
819baf9
fix(e2e): use NSLog instead of os_log for CI-visible diagnostics
dianaKhortiuk-frontegg Apr 6, 2026
d4414d5
fix(e2e): pin FronteggSwift to master, set frontegg-testing env var
dianaKhortiuk-frontegg Apr 7, 2026
130d9b5
ci(e2e): use macos-15-xlarge for iOS jobs (matches frontegg-ios-swift)
dianaKhortiuk-frontegg Apr 7, 2026
ff687fc
ci(e2e): reset SwiftPM cache, verify FronteggSwift pin, log env state
dianaKhortiuk-frontegg Apr 7, 2026
25ad1d0
ci(e2e): treat Patrol CLI PatrolLogReader crash as pass when tests pa…
dianaKhortiuk-frontegg Apr 7, 2026
e7247e4
fix(e2e): set frontegg-testing env var via Obj-C +load (before main)
dianaKhortiuk-frontegg Apr 7, 2026
b60b552
Revert "fix(e2e): set frontegg-testing env var via Obj-C +load (befor…
dianaKhortiuk-frontegg Apr 7, 2026
c82c4e3
ci(e2e): exclude webview-login tests on iOS until launchEnvironment p…
dianaKhortiuk-frontegg Apr 7, 2026
0ccbed6
ci(e2e): enable webview-login tests on Android
dianaKhortiuk-frontegg Apr 7, 2026
e37b507
fix(android-e2e): re-subscribe state listener after rebinding singleton
dianaKhortiuk-frontegg Apr 7, 2026
ed73d93
ci(e2e): revert Android login-test enablement — second blocker remains
dianaKhortiuk-frontegg Apr 8, 2026
202a1f1
ci(e2e): retry on Patrol CLI pre-test-result crashes
dianaKhortiuk-frontegg Apr 8, 2026
d80ce73
ci(e2e): make artifact uploads non-fatal (GHA blob-storage ENOTFOUND …
dianaKhortiuk-frontegg Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/scripts/combine_e2e_summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
"use strict";

const fs = require("node:fs");
const path = require("node:path");

function main() {
const args = process.argv.slice(2);
let artifactsDir = "";
let summaryFile = "";

for (let i = 0; i < args.length; i++) {
if (args[i] === "--artifacts-dir" && args[i + 1]) artifactsDir = args[++i];
if (args[i] === "--summary" && args[i + 1]) summaryFile = args[++i];
}

if (!artifactsDir) {
console.error("Usage: combine_e2e_summary.js --artifacts-dir <dir> --summary <file>");
process.exit(1);
}

const lines = ["# Flutter Embedded E2E Summary\n"];
let totalTests = 0;
let totalPassed = 0;
let totalFailed = 0;

if (fs.existsSync(artifactsDir)) {
const entries = fs.readdirSync(artifactsDir);
for (const entry of entries.sort()) {
const entryPath = path.join(artifactsDir, entry);
if (!fs.statSync(entryPath).isDirectory()) continue;

lines.push(`## ${entry}\n`);

const xmlFiles = (fs.readdirSync(entryPath) || []).filter((f) => f.endsWith(".xml"));
if (xmlFiles.length === 0) {
lines.push("No JUnit XML reports found.\n");
continue;
}

for (const xmlFile of xmlFiles) {
const content = fs.readFileSync(path.join(entryPath, xmlFile), "utf-8");
const testsMatch = content.match(/tests="(\d+)"/);
const failuresMatch = content.match(/failures="(\d+)"/);
const errorsMatch = content.match(/errors="(\d+)"/);

const tests = testsMatch ? parseInt(testsMatch[1], 10) : 0;
const failures = (failuresMatch ? parseInt(failuresMatch[1], 10) : 0) +
(errorsMatch ? parseInt(errorsMatch[1], 10) : 0);
const passed = tests - failures;

totalTests += tests;
totalPassed += passed;
totalFailed += failures;

const emoji = failures > 0 ? "❌" : "✅";
lines.push(`${emoji} **${xmlFile}**: ${passed}/${tests} passed\n`);
}
}
}

lines.unshift("");
lines.unshift(`**Total**: ${totalPassed}/${totalTests} passed, ${totalFailed} failed`);
lines.unshift("");

const output = lines.join("\n");

if (summaryFile) {
fs.writeFileSync(summaryFile, output);
console.log(`Summary written to ${summaryFile}`);
} else {
console.log(output);
}
}

main();
126 changes: 126 additions & 0 deletions .github/scripts/generate_e2e_matrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env node
"use strict";

const fs = require("node:fs");
const path = require("node:path");

// One scenario per shard: pairs of heavy tests (login + 240s token waits) still
// exceeded 90m; isolating avoids a single stuck/hung test blocking a whole pair.
const MAX_TESTS_PER_SHARD = 1;

const ROOT = path.resolve(__dirname, "../..");
const CONFIG = {
catalog: path.join(ROOT, "embedded/e2e/scenario-catalog.json"),
testSources: [
path.join(ROOT, "embedded/integration_test/e2e/embedded_e2e_test.dart"),
],
};

function readCatalogMethods(catalogPath) {
const raw = JSON.parse(fs.readFileSync(catalogPath, "utf-8"));
const entries = raw.tests || raw.scenarios || [];
return entries
.map((entry) => entry.method)
.filter((method) => typeof method === "string" && method.length > 0);
}

function readDartTestMethods(testSources) {
const methods = new Set();
const re = /e2ePatrolTest\(\s*'(test[A-Za-z0-9_]+)'/gm;
for (const sourcePath of testSources) {
const source = fs.readFileSync(sourcePath, "utf-8");
for (const match of source.matchAll(re)) {
methods.add(match[1]);
}
}
return [...methods];
}

function parseExcludeList(raw) {
return new Set(
String(raw || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
);
}

function validateCatalog(catalogMethods, sourceMethods) {
const catalogSet = new Set(catalogMethods);
const sourceSet = new Set(sourceMethods);
const catalogOnly = catalogMethods.filter((m) => !sourceSet.has(m));
const sourceOnly = sourceMethods.filter((m) => !catalogSet.has(m));
if (catalogOnly.length === 0 && sourceOnly.length === 0) return;
const problems = [];
if (catalogOnly.length) problems.push(`catalog-only: ${catalogOnly.join(", ")}`);
if (sourceOnly.length) problems.push(`uncatalogued: ${sourceOnly.join(", ")}`);
throw new Error(`embedded E2E catalog drift: ${problems.join("; ")}`);
}

/** When building a platform-specific shard list, excluded tests stay in the repo/catalog but are omitted from the matrix. */
function validateShardMethods(shardMethods, sourceMethods) {
const sourceSet = new Set(sourceMethods);
const unknown = shardMethods.filter((m) => !sourceSet.has(m));
if (unknown.length) {
throw new Error(`shard methods not found in Dart sources: ${unknown.join(", ")}`);
}
}

function splitIntoShards(items, shardCount) {
const shards = Array.from({ length: shardCount }, () => []);
items.forEach((item, i) => shards[i % shardCount].push(item));
return shards;
}

function main() {
const parsed = parseInt(process.env.INPUT_SHARD_COUNT || "1", 10);
const shardCount = Number.isNaN(parsed) ? 1 : Math.max(1, parsed);

const catalogMethods = readCatalogMethods(CONFIG.catalog);
const exclude = parseExcludeList(process.env.INPUT_EXCLUDE_METHODS);
const methods = exclude.size
? catalogMethods.filter((m) => !exclude.has(m))
: catalogMethods;
const sourceMethods = readDartTestMethods(CONFIG.testSources);
if (exclude.size) {
for (const m of exclude) {
if (!catalogMethods.includes(m)) {
throw new Error(`INPUT_EXCLUDE_METHODS: not in catalog: ${m}`);
}
}
validateShardMethods(methods, sourceMethods);
} else {
validateCatalog(catalogMethods, sourceMethods);
}

const autoShards = Math.ceil(methods.length / MAX_TESTS_PER_SHARD);
const effectiveShardCount =
shardCount > 1 ? Math.min(shardCount, methods.length || 1) : Math.max(1, autoShards);

const include = [];
if (effectiveShardCount <= 1 || methods.length === 0) {
include.push({
"shard-index": 1,
"shard-total": 1,
"test-methods": "",
});
} else {
const shards = splitIntoShards(methods, effectiveShardCount);
shards.forEach((shard, i) => {
include.push({
"shard-index": i + 1,
"shard-total": effectiveShardCount,
"test-methods": shard.join(","),
});
});
}

const matrix = JSON.stringify({ include });
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
fs.appendFileSync(outputFile, `matrix=${matrix}\n`);
}
console.log(matrix);
}

main();
114 changes: 114 additions & 0 deletions .github/scripts/run_embedded_e2e_shard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
set -euo pipefail

METHODS="${E2E_METHODS:-}"
MAX_RETRIES="${PATROL_MAX_RETRIES:-1}"

cd embedded
TEST_FILE="integration_test/e2e/embedded_e2e_test.dart"

flutter pub get

run_patrol() {
local -a cmd=(patrol test)
if [[ -n "${IOS_DEVICE:-}" ]]; then
cmd+=(-d "$IOS_DEVICE")
fi
cmd+=(-t "$TEST_FILE")
cmd+=("$@")
cmd+=(--uninstall)
if [[ "${PATROL_VERBOSE:-}" == "1" ]]; then
cmd+=(--verbose)
fi
local capture
capture="$(mktemp)"
set +e
if [[ -n "${PATROL_LOG_FILE:-}" ]]; then
"${cmd[@]}" 2>&1 | tee -a "$PATROL_LOG_FILE" | tee "$capture"
local rc="${PIPESTATUS[0]}"
else
"${cmd[@]}" 2>&1 | tee "$capture"
local rc="${PIPESTATUS[0]}"
fi
set -e
# Patrol CLI 3.6.0 has a known bug in PatrolLogReader (`Bad state: No element`)
# that crashes the wrapper, returning a non-zero exit code.
#
# Case 1: crash AFTER every test passed — treat as pass.
# Case 2: crash BEFORE any test result was reported — flake, let the
# outer retry loop try again instead of failing the shard.
# Case 3: crash WITH an observed FAILED result — genuine failure, propagate.
if [[ "$rc" -ne 0 ]] && grep -q "Bad state: No element" "$capture" \
&& grep -q "PatrolLogReader.readEntries" "$capture"; then
local has_failed_result
has_failed_result=0
if grep -q "test result: FAILED" "$capture"; then has_failed_result=1; fi
if grep -q "❌ Failed: [1-9]" "$capture"; then has_failed_result=1; fi

if [[ "$has_failed_result" -eq 0 ]]; then
if grep -q "❌ Failed: 0" "$capture" && grep -q "✅ Successful:" "$capture"; then
echo "::warning::Patrol CLI crashed in PatrolLogReader after success — treating as pass"
rm -f "$capture"
return 0
fi
if grep -q "test result: PASSED" "$capture"; then
echo "::warning::Patrol CLI crashed in PatrolLogReader after success — treating as pass"
rm -f "$capture"
return 0
fi
# Crash BEFORE any test result was emitted — pure Patrol flake.
# Return a distinct exit code so the outer retry loop retries.
echo "::warning::Patrol CLI crashed in PatrolLogReader before any test result — treating as flake, will retry"
rm -f "$capture"
return 77
fi
fi
rm -f "$capture"
return "$rc"
}

# Best-effort recovery between retries (Android emulator / adb flakes, e.g. exit 224).
recover_between_retries() {
if command -v adb >/dev/null 2>&1; then
adb kill-server || true
adb start-server || true
sleep 3
fi
}

run_with_retries() {
local -a patrol_args=("$@")
local attempt=1
local status=0
while [[ "$attempt" -le "$MAX_RETRIES" ]]; do
if [[ "$attempt" -gt 1 ]]; then
echo "::warning::Patrol attempt $((attempt - 1)) failed; retrying ($attempt/$MAX_RETRIES)..."
recover_between_retries
fi
set +e
run_patrol "${patrol_args[@]}"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
return 0
fi
attempt=$((attempt + 1))
done
# Normalize sentinel exit codes (77 = patrol flake, retries exhausted) to 1
# so the shell step fails with a clean non-zero rather than a surprising code.
if [[ "$status" -eq 77 ]]; then
return 1
fi
return "$status"
}

# One patrol invocation per shard: comma-separated E2E_TEST_FILTER matches multiple
# e2ePatrolTest entries without paying for N× iOS xcodebuild + install cycles.
if [[ -n "$METHODS" ]]; then
echo "::notice::Running E2E shard tests: $METHODS"
run_with_retries --dart-define="E2E_TEST_FILTER=$METHODS"
exit $?
fi

echo "::notice::Running full E2E suite"
run_with_retries
Loading
Loading