diff --git a/.github/workflows/testrigor.yml b/.github/workflows/testrigor.yml new file mode 100644 index 0000000000000..44a39b535eb77 --- /dev/null +++ b/.github/workflows/testrigor.yml @@ -0,0 +1,157 @@ +# Required repository secrets: +# TESTRIGOR_API_KEY — Personal Auth Token (username → API Tokens) +# (falls back to TESTRIGOR_AUTH_TOKEN if set) +# +# Required repository variable: +# TESTRIGOR_TEST_SUITE_ID — suite ID from the suite URL +# +# Optional repository variable: +# APPFLOWY_WEB_URL — app under test (default: https://appflowy.com/app) + +name: testRigor + +on: + # Runs on push to feature branches — not on main. + push: + branches-ignore: + - main + paths: + - ".github/workflows/testrigor.yml" + - "testRigor/**" + + workflow_dispatch: + +concurrency: + group: testrigor-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + testrigor: + name: Run testRigor suite + runs-on: ubuntu-latest + timeout-minutes: 90 + env: + TESTRIGOR_TEST_SUITE_ID: ${{ vars.TESTRIGOR_TEST_SUITE_ID }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install testRigor CLI + run: | + set -euo pipefail + npm install -g testrigor-cli + testrigor --version + + - name: Run testRigor suite + env: + TESTRIGOR_API_KEY: ${{ secrets.TESTRIGOR_API_KEY }} + TESTRIGOR_AUTH_TOKEN: ${{ secrets.TESTRIGOR_AUTH_TOKEN }} + run: | + set -euo pipefail + + # CLI expects TESTRIGOR_API_KEY (PAT). Allow the older suite auth-token secret as fallback. + if [ -z "${TESTRIGOR_API_KEY:-}" ] && [ -n "${TESTRIGOR_AUTH_TOKEN:-}" ]; then + export TESTRIGOR_API_KEY="${TESTRIGOR_AUTH_TOKEN}" + fi + + if [ -z "${TESTRIGOR_API_KEY:-}" ]; then + echo "::error::Missing secret TESTRIGOR_API_KEY (or TESTRIGOR_AUTH_TOKEN)" + exit 1 + fi + + if [ -z "${TESTRIGOR_TEST_SUITE_ID:-}" ]; then + echo "::error::Missing variable TESTRIGOR_TEST_SUITE_ID" + exit 1 + fi + + APP_URL="${{ vars.APPFLOWY_WEB_URL }}" + if [ -z "${APP_URL}" ]; then + APP_URL="https://appflowy.com/app" + fi + echo "App under test: ${APP_URL}" + echo "Suite ID: ${TESTRIGOR_TEST_SUITE_ID}" + + mkdir -p testRigor/.run + + # Sync mode (no --async): exit code gates the job. + # --explicit-mutations: local testRigor/testcases (+ rules) is authoritative — + # only those cases are kept/updated on the suite and executed. Remote-only + # cases not present as files are dropped from the suite. + testrigor test-suite run "${TESTRIGOR_TEST_SUITE_ID}" \ + --url "${APP_URL}" \ + --test-cases-path "testRigor/testcases/**/*.{txt,yaml,yml}" \ + --rules-path "testRigor/rules/**/*.{txt,yaml,yml}" \ + --explicit-mutations \ + --branch "${GITHUB_REF_NAME}" \ + --commit "${GITHUB_SHA}" \ + --junit-report-save-path "testRigor/.run/report.xml" \ + --verbose + + # Diagnostic only: the JUnit report identifies cases as "Test Case UUID=" with + # an empty uuid, so a failure cannot be mapped back to a file without this. + - name: Report per-case results + if: always() + env: + TESTRIGOR_API_KEY: ${{ secrets.TESTRIGOR_API_KEY }} + TESTRIGOR_AUTH_TOKEN: ${{ secrets.TESTRIGOR_AUTH_TOKEN }} + run: | + set -uo pipefail + + TOKEN="${TESTRIGOR_API_KEY:-}" + if [ -z "${TOKEN}" ]; then TOKEN="${TESTRIGOR_AUTH_TOKEN:-}"; fi + if [ -z "${TOKEN}" ]; then + echo "No token available; skipping per-case report." + exit 0 + fi + + curl -sS --max-time 60 \ + -H "Accept: application/json" \ + -H "auth-token: ${TOKEN}" \ + "https://app.testrigor.com/api/v1/apps/${TESTRIGOR_TEST_SUITE_ID}/status?branchName=${GITHUB_REF_NAME}" \ + -o testRigor/.run/status.json || true + + python3 - <<'PY' + import json, os, sys + + path = "testRigor/.run/status.json" + try: + with open(path, encoding="utf-8") as fh: + data = json.load(fh) + except Exception as exc: + print(f"Could not parse {path}: {exc}") + sys.exit(0) + + print(f"status: {data.get('status')}") + print(f"details: {data.get('detailsUrl')}") + + errors = data.get("errors") or [] + if not errors: + print("No per-case errors reported.") + sys.exit(0) + + for err in errors: + names = ", ".join( + tc.get("name", "?") for tc in (err.get("affectedTestCases") or []) + ) or "?" + print("") + print(f"[{err.get('severity')}/{err.get('category')}] {names}") + print(f" error: {err.get('error')}") + print(f" step: {err.get('greatestStepN')} occurrences: {err.get('occurrences')}") + print(f" url: {err.get('detailsUrl')}") + + sys.exit(0) + PY + + - name: Upload JUnit report + if: always() + uses: actions/upload-artifact@v4 + with: + name: testrigor-junit-report + path: testRigor/.run/report.xml + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 33de28002aae3..7924f19eca0fa 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ frontend/*.deb **/Cargo.toml.bak -**/.cargo/** \ No newline at end of file +**/.cargo/** + +# testRigor local run artifacts +testRigor/.run/ \ No newline at end of file diff --git a/testRigor/Settings.yaml b/testRigor/Settings.yaml new file mode 100644 index 0000000000000..51e4ac53b291d --- /dev/null +++ b/testRigor/Settings.yaml @@ -0,0 +1,290 @@ +monitoringIntervals: -1 +clickButtonOnlyOnce: false +detectUiIssues: false +axeCoreTestStrategy: DO_NOT_RUN +maxAxeCoreErrorSeverity: MAJOR +aiScreenshotTestStrategy: DO_NOT_RUN +maxAiScreenshotTestErrorSeverity: BLOCKER +screenRereadStrategy: UsePreviousVersion +anchorSearchRereadStrategy: REREAD_AFTER_FINDING_ANCHOR +combinedStepsStrategy: ENABLED +accessibilityInfoType: NO_ACCESSIBILITY_INFO +parentPathStrategy: AllPath +useParallelCombinator: true +maximumParallelServers: 10 +maxParallelBatchSize: 10 +allowedRecordedSessionTimeouts: 12 +maximumNumberOfAiFlows: 1 +appDescriptionForAi: "" +maximumNumberOfFlows: 0 +maximumNumberOfRecordedTestCases: 10 +maximumFlowDepth: 16 +maximumRecordedFlowDepth: 16 +maximumWaitTimeout: 30 +maximumWaitTimeoutForConditionals: 3 +maximumDataLoadTimeout: 60 +maximumDownloadTimeout: 60 +maximumEmailWaitTime: 60 +waitForPageToRender: 0 +waitFirstPage: 0 +waitAfterLogin: 0 +waitAfterSearch: 0 +waitAfterDataEntry: 0 +waitBeforeDropdownReread: 100 +waitAfterScrolling: 0 +waitAfterScrollingBeforeGettingElementImage: 500 +waitAfterUploadClick: 1 +waitAfterFillingForm: 1000 +waitAfterSwitchingTab: 1000 +waitAfterPressingKeys: 50 +waitAfterPressingKeysAndroid: 0 +waitAfterDrag: 100 +waitAfterElementFoundBeforeEngaging: 50 +delayBetweenMouseClickActions: 50 +delayBetweenMouseDragAndDropActions: 400 +longClickDuration: 3 +retryIntervalTime: 500 +liveScreenshotRefreshIntervalTime: 3 +maximumNumerOfRetries: 0 +delayAfterHideKeyboard: 50 +ignoreElements: [] +ignoreElementsAtCompare: [] +alwaysTestAllElementsInLists: false +includeAllElements: [] +customCookies: [] +extraHeaders: [] +customAttributes: [] +discoveryStrategy: DISCOVER +screenRetestStrategy: LAST +desktopWebClickStrategy: SELENIUM_JS +recordTestExecutionVideo: false +webSafariClickStrategy: NATIVE +webLongTextSendsKeyStrategy: SEND_ALL +retestTexts: ALL +retestDataInputs: ALL +retestControls: ALL +retestLists: ALL +testJavascriptErrors: NONE +testCssFiles: NONE +testImageFiles: NONE +testJavascriptFiles: NONE +testApiCalls: NONE +testOtherFiles: NONE +allowSubdomainsChange: ALLOW_ANY +customDomains: [] +autoLaunchMobileApplication: AUTOMATIC +mobileWebClickStrategy: CLICK +mobileEnterStrategy: DEFAULT +mobileKeyboardStrategy: CLICK_OUTSIDE +androidKeyboardStrategy: NAVIGATE_BACK +androidCloseKeyboardRetries: 3 +notificationStrategy: FULL +enableEmailNotifications: true +notificationsEmailMode: ALWAYS +notificationsEmail: amanda.silva@testrigor.com +enableSlackNotifications: false +notificationsSlackMode: ALWAYS +enableMicrosoftTeamsNotifications: false +notificationsMicrosoftTeamsMode: ALWAYS +microsoftTeamsNotificationsEmail: "" +allowAiButtonPrediction: false +newTabsStrategy: SWITCH_TO_NEW +dropdownDetectionStrategy: OPTIMISTIC +elementPriorizationStrategy: MAX +mobileAutoAcceptPermissions: false +iosSimAppPermissions: + enabled: false +logLevel: DEBUG +useRecordedData: false +useRecordedFlowsWithXpaths: false +jira: + allowJiraIntegration: false + allowJiraApiIntegration: false +hiptest: + allowHipTestIntegration: false +codeGeneration: + enabled: false + executorType: TR_INFRA + repositories: [] + pullRequestLabelsOnCompleted: [] + pullRequestLabelsOnIncomplete: [] + commitProduceStrategy: ALL_CHANGES_IN_ONE_COMMIT + codeGenerationModelType: OPUS + codeReviewModelType: OPUS +testRail: + enabled: false + importStrategy: IMPORT_STEPS_AND_EXPECTED + empty: true +practiTest: + enabled: false + empty: true +allureTestOps: + enabled: false + empty: true +twilio: {} +jiraPlugin: + enabled: false +browserStack: + enabled: false + enableTunnel: false + enabledAndValid: false +mozark: + enabled: false + enableTunnel: false + valid: false + enabledAndValid: false +lambdaTest: + enabled: false + enableTunnel: false + enabledAndValid: false +sauceLabs: + enabled: false + enableTunnel: false + enableImageInjection: false + enabledAndValid: false +kobiton: + enabled: false + enableTunnel: false + enabledAndValid: false +jdbcDatabase: + enabled: false + configs: [] + proxies: [] +pagerDuty: + enabled: false +applitools: + enabled: false +reportPortal: + ignoreSSLErrors: false + collectStepStatistics: false + enabled: false + attributes: [] + scheduledRunAttributes: [] +telnyx: + valid: false +phoneProvider: TWILIO +zephyr: + enabled: false + flavor: enterprise + trackingStrategy: project + empty: true +screenWidth: 1280 +screenHeight: 1024 +deviceOrientation: PORTRAIT +deviceLanguage: en_US +browserLanguage: en_US +seleniumChromeHeadlessMode: false +incognitoMode: true +disableHttpNotSecureWarning: IGNORE_MAIN_URL_ONLY +geolocationPermitStrategy: ALLOW +validateCertificates: IGNORE +fullPageScreenshot: false +blockNotifications: true +incognitoDownloadBehavior: AUTO_RENAME +pageReadStrategy: SHORT_XPATH +webPageInfoStrategy: JQUERY_VISIBLE +ocrProcessing: false +alwaysStartNewDriver: true +maximumFrameLevels: 1 +waitPageNonEmptyStrategy: STOP_EXECUTION_IF_EMPTY_AFTER_TIMEOUT +waitForPageToChangeAfterClickOrDrag: ONLY_BEFORE_AI_CODE_GENERATION +measurePageLoad: true +measureTimeBetweenActions: true +waitForScripts: false +automaticAlertHandler: false +loadTestMonitoringInterval: + frequency: -1 +loadTestConfiguration: + numberOfServers: 10 + durationInMinutes: 10 + failOption: never +androidWaitForIdleTimeout: 10000 +androidWaitForSelectorTimeout: 10000 +disabledWebViewContext: false +desktopWebScreenshotStrategy: VIEWPORT_USING_OS +androidDisabledWindowAnimation: false +durationOfHoldForDragAndDropOnIos: 500 +snapshotMaxDepth: 50 +azureDevOps: + allowAzureDevOpsIntegration: false + childrensDepthClassificationNode: 20 + empty: true +xrayCloud: + enabled: false + customFields: [] + jiraCustomFields: [] + empty: true +specificationLanguage: ENGLISH +scrollLength: WITH_STICKY_97 +horizontalScrollStrategy: CENTER +allCombinations: false +disableReadWithoutSourceFirst: true +chromeExtensionPreferences: + enabled: false + extensions: [] + extensionNames: [] +userDefinedCapabilities: [] +useSynonymsOfDetectors: false +forceToUseDeepestElementInHierarchy: true +ignoreInnerChildButtons: true +showActualVersions: true +allowPdfPreview: false +considerSwitchAsButton: SWITCHS_AND_BUTTONS +scrollBeforeClickStrategy: AUTOMATIC +scrollBeforeEnterStrategy: AUTOMATIC +scrollBeforeCheckStrategy: AUTOMATIC +scrollBeforeAnchorSearchStrategy: AUTOMATIC +treatAllSpanAsLabel: false +isSupportSecondaryRoleValueListOfButton: true +isSupportSecondaryRoleValueListOfSwitch: true +showHttpRequestInfo: false +iosVisibilityStrategy: USE_COORDINATES +aiPreferences: + delayAfterLoginStepInSeconds: 10 + delayBeforeProcessPageByAiInMs: 300 + autoHealReusableRules: DISABLED + autoHealSingleCommands: DISABLED + autoHealValidation: DISABLED + aiRuleCaching: TEST_SUITE_LEVEL + aiEvaluationStrategy: SINGLE_CALL + timeoutForAiCallInSeconds: 300 + appContextImages: [] +timezone: America/Los_Angeles +monitoringTimezone: America/Bahia +monitoringBranch: BRANCH +sourceExtraMobileAppFiles: [] +extraMobileAppFiles: [] +clientCertificates: [] +maximumNumberOfSteps: 5000 +clearTextSelectionStrategy: CTRL_A +shadowDomSupport: ENABLED +waitUpToTimesUntilStrategy: CALCULATE_TIME_AND_TIMEOUT +maxScrollsToEndOfPage: 42 +useOpacityToDetermineVisibilityWeb: USE_OPACITY +delayBetweenSendKeys: 10 +overlappedElementsStrategy: DONT_ALLOW_OVERLAPPED +screenshotOrderMobileNative: BEFORE_SOURCE +iosClickInvisibleButtonStrategy: CLICK +prioritizeVisibleTextStrategy: VISIBLE_TEXT_FIRST +valueAttributePrioritizationStrategy: IGNORE_VALUE +enterValueElementMatchStrategy: IGNORE_VALUE_TYPE +restrictParallelization: false +axeCoreTestStandards: [] +labelTextStrategy: LABEL_TEXT +handleAlertStrategy: ACCEPT +clickHoverDragActionErrorTreatment: STOP_EXECUTION +enterActionErrorTreatment: PROCEED_EXECUTION_WITH_CRITICAL_ERROR +validationActionErrorTreatment: PROCEED_EXECUTION_WITH_CRITICAL_ERROR +customActionErrorTreatment: STOP_EXECUTION +parsingMode: STRICT_WITH_AI_FALLBACK +allowedPropertiesForElementSearchInActions: VISIBLE_ONLY +allowedPropertiesForElementSearchInValidationActions: VISIBLE_ONLY +printCommentsOnReports: DO_NOT_PRINT +showVariableValues: SHOW_VARIABLE_VALUES +coordinateCacheUseStrategy: USE +failedStepTreatmentStrategy: PROCEED_IF_POSSIBLE +tableConditionFindingStrategy: FIND_MATCHING +maximumNumberOfPreviousClickRetries: 2 +enteredValueWhitespaceStrategy: TRIM +relativeLocationTextNodesIntersectionStrategy: ALL +emptyDetectorResolutionStrategy: STOP_EXECUTION diff --git a/testRigor/rules/Delete account.txt b/testRigor/rules/Delete account.txt new file mode 100644 index 0000000000000..f83e3f6c19d67 --- /dev/null +++ b/testRigor/rules/Delete account.txt @@ -0,0 +1,9 @@ +click "My Workspace" +click "Settings" +scroll down on "Account & App" +scroll down on "Language" +click "Delete account" +click "Delete account" +check that "Delete account" is not clickable +reload +check that page does not contain "Delete account" diff --git a/testRigor/rules/Delete current document.txt b/testRigor/rules/Delete current document.txt new file mode 100644 index 0000000000000..14c38adc984c8 --- /dev/null +++ b/testRigor/rules/Delete current document.txt @@ -0,0 +1,4 @@ +click on 1st button to the right of "Share" +check that page contains "Delete" +click "Delete" +check that page does not contain stored value "newTitle" diff --git a/testRigor/rules/Login using password.txt b/testRigor/rules/Login using password.txt new file mode 100644 index 0000000000000..9a1a2a5e0398d --- /dev/null +++ b/testRigor/rules/Login using password.txt @@ -0,0 +1,5 @@ +enter saved value "username" into "Please enter your email address" +click "Continue with password" +enter stored value "password" into "Password" +click "Continue" +check that page contains "New Page" diff --git a/testRigor/rules/Sign up using password.txt b/testRigor/rules/Sign up using password.txt new file mode 100644 index 0000000000000..1be62ea61d686 --- /dev/null +++ b/testRigor/rules/Sign up using password.txt @@ -0,0 +1,14 @@ +click "Sign up here" +generate unique email, then enter into "Please enter your email address" and save as "username" +enter stored value "password" into "Password" +enter stored value "password" into "Repeat Password" +click "Sign Up" +check that email to saved value "username" was received +click "Login to AppFlowy" +switch to tab 1 +click "Back to login" +enter saved value "username" into "Please enter your email address" +click "Continue with password" +enter stored value "password" into "Password" +click "Continue" +check that page contains "Welcome" diff --git "a/testRigor/rules/custom enter \"value\" into \"input\".txt" "b/testRigor/rules/custom enter \"value\" into \"input\".txt" new file mode 100644 index 0000000000000..9625a168f952a --- /dev/null +++ "b/testRigor/rules/custom enter \"value\" into \"input\".txt" @@ -0,0 +1,5 @@ +click stored value "input" +type ctrl + a +type delete +type stored value "value" +type enter diff --git a/testRigor/structure.yaml b/testRigor/structure.yaml new file mode 100644 index 0000000000000..9c0a0e8e6b123 --- /dev/null +++ b/testRigor/structure.yaml @@ -0,0 +1,4 @@ +testCases: ./testcases +rules: ./rules +testData: ./testdata +datasets: ./datasets diff --git "a/testRigor/testcases/Board_(Kanban)_\342\200\224_full_regression__cards,_groups,_hidden_groups,_fields.txt" "b/testRigor/testcases/Board_(Kanban)_\342\200\224_full_regression__cards,_groups,_hidden_groups,_fields.txt" new file mode 100644 index 0000000000000..3d9d6e54bed6e --- /dev/null +++ "b/testRigor/testcases/Board_(Kanban)_\342\200\224_full_regression__cards,_groups,_hidden_groups,_fields.txt" @@ -0,0 +1,163 @@ +// ============================================================ +// Board (Kanban view) — full regression +// Suite start URL opens AppFlowy Web. Logs in, creates a fresh +// Kanban board (ships with groups No Status / To Do / Doing / Done +// and sample cards Card 1..3 in To Do), then exercises the plan +// ============================================================ +// ---------- SETUP: log in and create a fresh Kanban board ---------- +Login using password +check that page contains "New page" +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +check that page contains "Enter a / to insert a block, or start typing" +enter enter +type "/board" +click "Kanban" +check that page "shows a kanban board with To Do column and cards" using ai +click "Open as a Page" using ai +check that page contains "To Do" +check that page contains "Card 1" +check that page contains "Card 3" +// ============================================================ +// ADDING CARDS +// ============================================================ +// --- Footer "+ New" button -> new card appears at the BOTTOM of the column --- +click "New" below "Card 3" +type "Bottom Card" +type enter +check that page contains "Bottom Card" below "Card 3" +// --- Group header "+" button -> new card appears at the TOP of the column --- +// Spatial "2nd button to the right of To Do" is flaky (often hits Doing's +) +hover over "Card 1" +click on "the plus (+) button in the To Do column header directly above Card 1, not the Doing or Done column headers" using ai +type "Top Card" +type enter +check that page contains "Top Card" +check that page contains "Top Card" above "Card 1" +// ============================================================ +// CARDS: edit inline / duplicate / delete +// ============================================================ +// --- Edit a card's text inline --- +custom enter "Card 2 Edited" into "Card 2" +type escape +check that page contains "Card 2 Edited" +// --- Duplicate a card -> a second card with the same name appears --- +hover over "Card 1" +click button "..." within the context of section "Card 1" using AI +check that page contains "Duplicate" +click "Duplicate" with retries if the next step fails +check that page contains 2nd "Card 1" +// --- Duplicate-then-delete -> back to one (delete shows a confirm dialog) --- +hover over the 2nd "Card 1" +click 2nd button containing "..." near text "Card 1" using ai +check that page contains "Delete" +click "Delete" +// a confirm dialog ("Are you sure you want to delete 1 row?") usually appears; its +// appearance is intermittent in automation, so confirm it when present, then assert removal +click "Delete" to the right of "Cancel" if exists +check that page doesn't contain 2nd "Card 1" +// --- Delete a card (with a confirm step); the card disappears --- +hover over "Bottom Card" +click button "three dots more options menu button" within the context of section "Bottom Card" using ai +check that page contains "Delete" +click "Delete" +click "Delete" to the right of "Cancel" if exists +check that page doesn't contain "Bottom Card" +// ============================================================ +// FIELDS (part 1) — edit a select option from the card's row detail +// Done here (before the group ops scroll the board right) so Card 1 is in view +// ============================================================ +click "Card 1" +check that page "shows the card detail panel with a Status field" using ai +click on label "Status" to the right of button containing "⋮" using ai +check that page contains "Add an option" +click text "..." to the right of text "To Do" using ai +check that page contains "Delete tag" +type ctrl + a +type "To Do Edited" +type enter +click the middle of the screen +check that page contains "To Do Edited" +type escape +// ============================================================ +// GROUPS (columns) +// ============================================================ +// --- Move a card to another group by dragging (updates its grouping value) --- +drag from button below "Card 2 Edited" to "Doing" +check that page contains "1" to the right of "Doing" +check that page contains "Card 3" +// --- Add a new group: text-field UX (type shows text, Escape cancels, submit creates) --- +// the "add a new group" control is the trailing "+" after the last column; hover "Done" +// to scroll it into view. The field has a blue "Done" submit button and NO clear button +// (clear exists only on desktop). Submit via the "Done" button — Enter does NOT submit +type escape +hover over "Done" +// (a) type a name, then Escape cancels without creating a group +// (the typed text lives in an input field, so a page-contains check of it is +// unreliable; we assert the meaningful outcome — Escape does not create the group) +click 3rd button to the right of "Done" +type "Cancelled Group" +type escape +check that page doesn't contain "Cancelled Group" +// (b) typing a name and submitting via the field's blue "Done" button (the last "Done" +// on the page) creates it — Enter does NOT submit +hover over "Done" +click 3rd button to the right of "Done" +type "QA Group" +click the last "Done" +check that page contains "QA Group" +// --- Rename a group --- +// open the group "..." menu (Rename / Hide / Delete) via the 1st button to the right of +// the group name; the rename field commits via its blue "Done" button (not Enter) +hover over "QA Group" +click 1st button to the right of "QA Group" +check that page contains "Rename" +click "Rename" +type ctrl + a +type "QA Renamed" +click the last "Done" +check that page contains "QA Renamed" +check that page doesn't contain "QA Group" +// ============================================================ +// HIDING / SHOWING GROUPS -- BUG FOUND +// ============================================================ +// SKIPPED: the group "..." > "Hide" action does not fire +// Verified over multiple runs and manually: the menu opens and "Hide" highlights +// but the group is never hidden and no Hidden Groups section is populated. Rename +// Delete from the same menu DO work. Re-enable if a working Hide interaction is found +// hover over "No Status" +// click 1st button to the right of "No Status" +// click "Hide" using ocr +// check that page does not contain "No Status" +// --- Delete a group (confirm dialog: deletes the group and its cards) --- +hover over "QA Renamed" +click 1st button to the right of "QA Renamed" +check that page contains "Delete" +click "Delete" +// group delete shows a confirm dialog ("delete this group and all the cards in it") +// confirm it when present, then assert the group is gone +click "Delete" to the right of "Cancel" if exists +check that page doesn't contain "QA Renamed" +// ============================================================ +// FIELDS (part 2) — change a field's type from a card's row detail +// (regression test). LAST because changing the group-by Status field's type is +// destructive to the board's grouping. Scroll left first: the group ops left +// board scrolled right, so Card 1 is off-screen +// ============================================================ +scroll left +scroll left +scroll left +click "Card 1" +check that page "shows the card detail panel" using ai +click on label "Status" to the right of ":::" using ai +check that page contains "Add an option" +click "Select" +check that page contains "Multiselect" +click "Text" +// regression assertion: changing the group-by field's type must not crash the row detail +// (a generic `check page for UI errors` is intentionally NOT used here — it false-positives +// on the deliberately-renamed "To Do Edited" option and the many leftover "Untitled" pages) +check that page "still shows the card detail panel without any crash or error" using ai +type escape diff --git a/testRigor/testcases/Document_Create,_Edit,_Delete,_Restore_and_Publish.txt b/testRigor/testcases/Document_Create,_Edit,_Delete,_Restore_and_Publish.txt new file mode 100644 index 0000000000000..68e0ca638241c --- /dev/null +++ b/testRigor/testcases/Document_Create,_Edit,_Delete,_Restore_and_Publish.txt @@ -0,0 +1,89 @@ +// ============================================================ +// Document Lifecycle (E2E) — AppFlowy Web +// create -> edit -> delete -> restore -> publish -> clean up +// Do not enter into "Title" (contenteditable is not interactable +// retries re-click Add and open the Share overlay) +// Stay in the new-page dialog — do not Escape (that closes it) +// Expand to full page before delete so Trash is not covered +// Short unique titles stay visible in the sidebar after restore +// ============================================================ +Login using password +check that page contains "New page" +// PART 1 — Create +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +check that page contains "Enter a / to insert a block, or start typing" +check that page contains "Untitled" +triple click "Untitled" +generate from regex "dq[a-z]{5}" and save as "newTitle" +type stored value "newTitle" +type enter +check that page contains stored value "newTitle" +enter enter +type "QA body text" +type enter +check that page contains "QA body text" +click on "the expand (open as page) icon at the top-left of the document dialog" using ai +check that page contains stored value "newTitle" +check that page contains "Share" +// PART 2 — Delete (full-page: Share | ★ | ⋯) +click on "the more options (...) button at the top-right of the page, to the right of the star favorite icon" using ai +check that page contains "Delete" +click "Delete" +click "Delete" to the right of "Cancel" if exists +check that page does not contain stored value "newTitle" +// PART 3 — Restore (do not assert "Trash" — that matches the icon tooltip) +click on "the trash bin icon at the bottom-left of the sidebar" using ai +check that page contains "Restore All" +check that page contains "Delete All" +check that page contains stored value "newTitle" below "File name" +hover over stored value "newTitle" below "File name" +click on "the Restore icon (counterclockwise arrow) on the same row as the stored file name in the trash list, not Restore All and not the row delete/trash icon" using ai +check that page does not contain stored value "newTitle" below "File name" +// Leave Trash via Shared (does not toggle General). Expand General with +// the chevron (2nd control left of the label), then open the page +click "Shared" +check that page contains "Shared" +hover over "General" +click 2nd button to the left of "General" +check that page contains stored value "newTitle" +click stored value "newTitle" +// PART 4 — Publish +check that page contains "My Workspace" +check that page does not contain "Comments" +click "Share" +click "Publish" +click "Publish" roughly below "Create a website with AppFlowy" +click "Visit site" +check that page contains stored value "newTitle" +check that page does not contain "My Workspace" +scroll down until page contains "Comments" +// Comments toggle +generate unique name , then enter into "Add a comment" and save as "newComment" +click "Add" +check that page contains saved value "newComment" roughly below "Comments" +switch to tab 1 +enter "0" into input to the right of "Comments" +switch to tab 2 +reload +check that page contains stored value "newTitle" +check that page does not contain "Comments" +// Template toggle +check that page contains "Start with this template" +switch to tab 1 +enter "0" into input to the right of "Duplicate as template" +switch to tab 2 +reload +check that page contains stored value "newTitle" +check that page does not contain "Start with this template" +// Clean Up — full-page: Share | ★ | ⋯ +switch to tab 1 +type escape +check that page contains "Share" +click on "the more options (...) button at the top-right of the page, to the right of the star favorite icon" using ai +check that page contains "Delete" +click "Delete" +click "Delete" to the right of "Cancel" if exists +check that page does not contain stored value "newTitle" diff --git a/testRigor/testcases/Favorites_&_Sub-pages_(E2E).txt b/testRigor/testcases/Favorites_&_Sub-pages_(E2E).txt new file mode 100644 index 0000000000000..23a7e853515f6 --- /dev/null +++ b/testRigor/testcases/Favorites_&_Sub-pages_(E2E).txt @@ -0,0 +1,44 @@ +// ============================================================ +// Favorites & Sub-pages (E2E) — AppFlowy Web +// Create a page, favorite/unfavorite, nest a sub-page via slash +// menu, verify breadcrumb, then clean up +// ============================================================ +Login using password +check that page contains "New page" +// ---- Create parent + open as full page ---- +click "New page" +click "General" +click "Add" +generate from regex "fp[a-z]{5}", then enter into "Title" and save as "newTitle" +check that page contains stored value "newTitle" +click on "the expand (open as page) icon at the top-left of the document dialog" using ai +check that page contains stored value "newTitle" +check that page contains "Share" +// ---- Favorite ---- +click on "the star (favorite) icon in the top-right toolbar of the open page" using ai +check that page contains "Favorites" +check that page contains stored value "newTitle" roughly below "Favorites" roughly above "General" +// ---- Unfavorite (tooltip — breadcrumb collides with spatial Favorites/General band) ---- +click on "the star (favorite) icon in the top-right toolbar of the open page" using ai +wait 1 second +hover over "the star (favorite) icon in the top-right toolbar of the open page" using ai +check that page contains "Add to Favorites" +// ---- Create sub-page via slash menu ---- +enter "/" into "Enter a / to insert a block, or start typing" +wait 1 second +type "sub page" +click "Document" +generate from regex "fc[a-z]{5}", then enter into "Title" and save as "childTitle" +check that page contains stored value "childTitle" +click on "the expand (open as page) icon at the top-left of the document dialog" using ai +check that page contains stored value "childTitle" +// ---- Verify nesting via breadcrumb (horizontal: parent is left of child) ---- +check that page contains stored value "newTitle" to the left of stored value "childTitle" +click stored value "newTitle" to the left of stored value "childTitle" +check that page contains stored value "childTitle" +// ---- Clean up parent ---- +click on "the more options (...) button at the top-right of the page, to the right of the star favorite icon" using ai +check that page contains "Delete" +click "Delete" +click "Delete" to the right of "Cancel" if exists +check that page does not contain stored value "newTitle" diff --git a/testRigor/testcases/Global_Search___Quick-find_(E2E).txt b/testRigor/testcases/Global_Search___Quick-find_(E2E).txt new file mode 100644 index 0000000000000..ab63884df8a59 --- /dev/null +++ b/testRigor/testcases/Global_Search___Quick-find_(E2E).txt @@ -0,0 +1,39 @@ +// ============================================================ +// Global Search / Quick-find (E2E) — AppFlowy Web +// 1) Empty-state search for a nonsense query +// 2) Create a page, then search for it by title +// Use `enter ... into input` for the modal field (only input while +// the Quick-find palette is open). Do not use AI clicks +// ============================================================ +Login using password +check that page contains "New page" +// ---- Empty-state search ---- +click "Search" +check that page contains "TAB" +check that page contains "Recent pages" +enter "zzqqxxnomatchquery" into input +check that page does not contain "Recent pages" +check that page contains "No search results" +check that page contains "Ask AI" +type escape +// ---- Create a page with a unique title ---- +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +check that page contains "Enter a / to insert a block, or start typing" +check that page contains "Untitled" +triple click "Untitled" +generate from regex "sq[a-z]{5}" and save as "newTitle" +type stored value "newTitle" +type enter +check that page contains stored value "newTitle" +click 2nd button to the right of "Share" in the context of "Add icon" +check that page contains "New page" +// ---- Search for the page just created ---- +click "Search" +check that page contains "TAB" +enter stored value "newTitle" into input +check that page does not contain "No search results" +check that page contains stored value "newTitle" +type escape diff --git a/testRigor/testcases/Grid_Row__Sort_and_Filter_Operations.txt b/testRigor/testcases/Grid_Row__Sort_and_Filter_Operations.txt new file mode 100644 index 0000000000000..996785567af48 --- /dev/null +++ b/testRigor/testcases/Grid_Row__Sort_and_Filter_Operations.txt @@ -0,0 +1,64 @@ +Login using password +check that page contains "New page" +// ---- create a Grid via the slash menu ---- +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +check that page contains "Enter a / to insert a block, or start typing" +enter enter +enter "/grid" +enter enter +check that page contains "New row" +// ---- populate the Name column (each value anchors the next row) ---- +click below "Name" +enter "Apple" +type escape +check that page contains "Apple" +click below "Apple" +enter "Banana" +type escape +check that page contains "Banana" +click below "Banana" +enter "Cherry" +type escape +check that page contains "Cherry" +// ---- Editing a text cell while a sort is active should cause the grid to re-sort live to the new position ---- +click 2nd button above "New property" +click "Name" below "Sort by" +// Dismiss the sort panel (Escape does not close it). It covers the top Name cells +click "New Database" +check that page contains "Apple" above "Banana" +custom enter "Zapple" into "Apple" +check that page doesn't contain exactly "Apple" +check that page doesn't contain "Zapple" above "Banana" +check that page contains "Zapple" below "Banana" below "Cherry" +click "Name" roughly to the left of "Add Filter" +check that page contains "Delete sorts" +click "Delete sorts" +custom enter "Apple" into "Zapple" +// ---- check the "Done" box on the Apple row only ---- +click on "the Done checkbox on the Apple row" using ai +check that page "shows Apple's Done checkbox checked and Banana/Cherry unchecked" using ai +// ---- add a filter on the Done checkbox field (defaults to "is checked") ---- +click "Add Filter" +check that page contains "Filter by" +click "Done" below "Filter by" +// Dismiss the filter panel so it does not cover Name cells +click "New Database" +// ---- only the checked row (Apple) should remain ---- +// Avoid is not visible / does not contain on Banana|Cherry (false positives) +check that page contains "Checked" +check that page contains "Apple" +check that page "shows a filtered grid with only the Apple row visible" using ai +// ---- unchecking Apple under an active Done:Checked filter should drop the row ---- +click on "the Done checkbox on the Apple row" using ai +click "New Database" +check that page "shows a filtered grid with no Apple, Banana, or Cherry name cells" using ai +// ---- delete the filter and confirm all rows return ---- +click on "the three-dot menu on the Done filter chip" using ai +check that page contains "Delete filter" +click "Delete filter" +check that page contains "Apple" +check that page contains "Banana" +check that page contains "Cherry" diff --git a/testRigor/testcases/Invite_new_user_and_accept_workspace_invite_(E2E).txt b/testRigor/testcases/Invite_new_user_and_accept_workspace_invite_(E2E).txt new file mode 100644 index 0000000000000..5db397a3037a3 --- /dev/null +++ b/testRigor/testcases/Invite_new_user_and_accept_workspace_invite_(E2E).txt @@ -0,0 +1,106 @@ +// ============================================================ +// Invite new user and accept workspace invite (E2E) — AppFlowy Web +// Validates the invited user's browser via invite link: +// generate link → Guest opens it → sign up → Join workspace → access +// Settings nav uses "People" (renamed from "Members") +// ============================================================ +// ---- Owner: fresh account ---- +Sign up using password +check that page contains "New page" +save saved value "username" as "ownerEmail" +save value "My Workspace" as "workspaceName" +// ---- Owner: generate invite link ---- +click "My Workspace" +check that page contains "Settings" +click "Settings" +check that page contains "People" +click "People" +check that page contains "Invite member by email" +click "generate a new link" +check that page contains "Copied link to clipboard" or "Invite link generated" +store clipboard value as "inviteUrl" +type escape +// ============================================================ +// PART — Invited user browser validation +// ============================================================ +start browser "Guest" and switch +open url stored value "inviteUrl" +check that url contains "invited" +check that page contains "Sign up here" +// Sign up as the invitee +click "Sign up here" +generate unique email, then enter into "Please enter your email address" and save as "inviteEmail" +enter stored value "password" into "Password" +enter stored value "password" into "Repeat Password" +click "Sign Up" +check that email to saved value "inviteEmail" was received +click "Login to AppFlowy" +switch to tab 1 +click "Back to login" +enter saved value "inviteEmail" into "Please enter your email address" +click "Continue with password" +enter stored value "password" into "Password" +click "Continue" +// Accept the invitation — wait for workspace UI to finish loading +check that page contains "You've been invited to join" +check that page contains saved value "workspaceName" +click "Join workspace" +check that page contains "New page" +check that page contains "Getting started" +check that page contains "Search" +check that page contains saved value "workspaceName" +// Invitee People view: self listed as Member +click on "the My Workspace switcher at the top-left of the sidebar" using ai +check that page contains "Settings" +check that page contains "Create workspace" +click "Settings" +check that page contains "People" +click "People" +check that page contains saved value "inviteEmail" +check that page contains "Member" roughly below saved value "inviteEmail" +type escape +// Invitee can create a page in the shared workspace +// Do not enter into "Title" (contenteditable is not interactable) +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +check that page contains "Enter a / to insert a block, or start typing" +check that page contains "Untitled" +triple click "Untitled" +generate from regex "ig[a-z]{5}" and save as "guestPage" +type stored value "guestPage" +type enter +check that page contains stored value "guestPage" +click 2nd button to the right of "Share" in the context of "Add icon" +check that page contains "New page" +// Owner confirms invitee is a Member +switch to browser "default" +click on "the My Workspace switcher at the top-left of the sidebar" using ai +check that page contains "Settings" +click "Settings" +check that page contains "People" +click "People" +check that page contains saved value "inviteEmail" +check that page contains "Member" roughly below saved value "inviteEmail" +type escape +// Clean up invitee (Member settings lack Language — avoid Delete account rule) +switch to browser "Guest" +click on "the My Workspace switcher at the top-left of the sidebar" using ai +check that page contains "Settings" +click "Settings" +click "Account & App" +scroll down until page contains "Delete account" +click "Delete account" +click "Delete account" +check that page contains "Welcome to AppFlowy" +// Clean up owner +switch to browser "default" +click on "the My Workspace switcher at the top-left of the sidebar" using ai +check that page contains "Settings" +click "Settings" +click "Account & App" +scroll down until page contains "Delete account" +click "Delete account" +click "Delete account" +check that page contains "Welcome to AppFlowy" diff --git a/testRigor/testcases/Invite_new_user_to_the_workspace.txt b/testRigor/testcases/Invite_new_user_to_the_workspace.txt new file mode 100644 index 0000000000000..3057025954799 --- /dev/null +++ b/testRigor/testcases/Invite_new_user_to_the_workspace.txt @@ -0,0 +1,84 @@ +// ============================================================ +// Workspace Management (E2E) — AppFlowy Web (login-based) +// create -> rename -> invite member by email -> delete workspace +// ============================================================ +login using password +check that page contains "New page" +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +// Do not enter into "Title" (contenteditable is not interactable). Wait for the +// editor to render, then set the title by replacing the "Untitled" placeholder. +check that page contains "Enter a / to insert a block, or start typing" +check that page contains "Untitled" +triple click "Untitled" +generate unique name and save it as "pageTitle" +type stored value "pageTitle" +type enter +check that page contains stored value "pageTitle" +// Type into the focused editor; the placeholder is not addressable as an input. +generate unique name and save as "textBody" +type stored value "textBody" +type enter +check that page contains stored value "textBody" +// Close the page dialog with the same descriptor the passing cases use. The +// loose "2nd button above and on the right of pageTitle" can land on the +// sidebar space chevron, collapsing "General" and hiding its pages. +click 2nd button to the right of "Share" in the context of "Add icon" +check that page contains "New page" +check that page doesn't contain saved value "textBody" +// 1. Fetch workspaces +// Do not click "General" — the section is already expanded (it holds the page +// just created), so clicking the label collapses it and hides its pages. The +// default pages only exist in that section, so no spatial qualifier is needed. +check that page contains "General" +check that page contains "Getting started" +check that page contains "To-dos" +click "My Workspace" +// 2. Create workspace +click "Create workspace" +// Enter a workspace name +generate unique name , then enter into "name" and save as "workspaceName" +// Submit +click "Create" +// Verify it becomes the active workspace +check that page doesn't contain "My Workspace" +// The new workspace's default pages are seeded asynchronously, so give the +// switch some slack before asserting them (the check itself waits 30s). +wait 10 seconds +check that page contains "Getting started" +click saved value "workspaceName" +// The switcher lists both workspaces; their order is not guaranteed (the active +// one is not necessarily last), so assert membership rather than a spatial relation. +check that page contains "My Workspace" +check that page contains saved value "workspaceName" +// ============================================================ +// PART 3 — Invite a new member by email (owner side) +// ============================================================ +click on "My Workspace" +click "Settings" +// Settings nav uses "People" (renamed from "Members"). +check that page contains "People" +click "People" +check that page contains "Invite member by email" +generate unique email, then enter into "Invite by email" and save as "inviteEmail" +click "Invite" +check that page contains "Invitation sent successfully" +check that page contains saved value "inviteEmail" below saved value "username" +check that page contains "Pending" roughly below saved value "inviteEmail" +enter escape +// Accept invite from second user +start browser "Guest" and switch +check that email to saved value "inviteEmail" was received +click link "Join Workspace" +check that url contains "invited" +check that page contains "Sign up here" +save saved value "inviteEmail" as "username" +Sign up using password +check that page contains saved value "workspaceName" +Delete account +switch to browser "default" +check that page doesn't contain "Pending" roughly below saved value "inviteEmail" +type escape +Delete account diff --git a/testRigor/testcases/Rich_Text_Blocks_&_Slash_Menu_(E2E).txt b/testRigor/testcases/Rich_Text_Blocks_&_Slash_Menu_(E2E).txt new file mode 100644 index 0000000000000..e7550a7d154ab --- /dev/null +++ b/testRigor/testcases/Rich_Text_Blocks_&_Slash_Menu_(E2E).txt @@ -0,0 +1,49 @@ +// ============================================================ +// Rich Text Blocks & Slash Menu (E2E) — AppFlowy Web +// Insert block types via the slash menu (type "/name" then Enter +// selects the top match) and assert each renders +// Stay in the new-page dialog — do not Escape (that closes it) +// ============================================================ +Login using password +check that page contains "New page" +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +check that page contains "Enter a / to insert a block, or start typing" +enter enter +// ---- Heading ---- +type "/heading" +type enter +type "Heading One" +type enter +check that page contains "Heading One" +// ---- To-do list ---- +type "/todo" +type enter +type "Todo item one" +type enter +check that page contains "Todo item one" +// ---- Quote ---- +type "/quote" +type enter +type "Quote text here" +type enter +check that page contains "Quote text here" +// ---- Callout ---- +type "/callout" +type enter +type "Callout content" +type enter +check that page contains "Callout content" +// ---- Code block ---- +type "/code" +type enter +type "codeword123" +check that page contains "codeword123" +// ---- Clean up (dialog: ... is left of the close X) ---- +click on "the more options (...) button at the top-right of the document dialog, to the left of the close (X) button" using ai +check that page contains "Delete" +click "Delete" +click "Delete" to the right of "Cancel" if exists +check that page does not contain "Heading One" diff --git a/testRigor/testcases/Sign_up,_create_a_new_workspace_and_delete_account.txt b/testRigor/testcases/Sign_up,_create_a_new_workspace_and_delete_account.txt new file mode 100644 index 0000000000000..ade327cc89910 --- /dev/null +++ b/testRigor/testcases/Sign_up,_create_a_new_workspace_and_delete_account.txt @@ -0,0 +1,62 @@ +Sign up using password +click "New page" +check that page contains "Where would you like to add" +click "General" +click "Add" +// Do not enter into "Title" (contenteditable is not interactable). Wait for the +// editor to render, then set the title by replacing the "Untitled" placeholder. +check that page contains "Enter a / to insert a block, or start typing" +check that page contains "Untitled" +triple click "Untitled" +generate unique name and save it as "pageTitle" +type stored value "pageTitle" +type enter +check that page contains stored value "pageTitle" +// Type into the focused editor; the placeholder is not addressable as an input. +generate unique name and save as "textBody" +type stored value "textBody" +type enter +check that page contains stored value "textBody" +click 2nd button above and on the right of saved value "pageTitle" +check that page doesn't contain saved value "textBody" +// 1. Fetch workspaces +// Do not click "General" — the section is already expanded (it holds the page +// just created), so clicking the label collapses it and hides its pages. The +// default pages only exist in that section, so no spatial qualifier is needed. +check that page contains "General" +check that page contains "Getting started" +check that page contains "To-dos" +click "My Workspace" +// 2. Create workspace +click "Create workspace" +// Enter a workspace name +generate unique name , then enter into "name" and save as "workspaceName" +// Submit +click "Create" +// Verify it becomes the active workspace +check that page doesn't contain "My Workspace" +// The new workspace's default pages are seeded asynchronously, so give the +// switch some slack before asserting them (the check itself waits 30s). +wait 10 seconds +check that page contains "Getting started" +click saved value "workspaceName" +// The switcher lists both workspaces; their order is not guaranteed (the active +// one is not necessarily last), so assert membership rather than a spatial relation. +check that page contains "My Workspace" +check that page contains saved value "workspaceName" +// 4. Rename workspace +click 1st button to the right of 2nd saved value "workspaceName" +click "Rename workspace" +generate unique name , then enter into "name" and save as "newWorkspaceName" +click "Rename" +click saved value "newWorkspaceName" +check that page doesn't contain saved value "workspaceName" +// 3. Delete workspace +click 1st button to the right of 2nd saved value "newWorkspaceName" +check that page contains "Rename workspace" above "Delete workspace" +click "Delete workspace" +click "Delete" +check that page doesn't contain saved value "newWorkspaceName" +check that page contains "My Workspace" +// 5. Delete account +Delete account diff --git a/testRigor/testdata/storedValues.yaml b/testRigor/testdata/storedValues.yaml new file mode 100644 index 0000000000000..f1c00541da63b --- /dev/null +++ b/testRigor/testdata/storedValues.yaml @@ -0,0 +1,4 @@ +storedValues: +- key: testVariable + value: this is a name. + type: TEXT