Skip to content

test(pro): seeded Pro accounts, over-limit pins, and the Clear All Data warning - #165

Merged
Bilb merged 24 commits into
mainfrom
test/pro-seeding-and-clear-data
Aug 28, 2026
Merged

test(pro): seeded Pro accounts, over-limit pins, and the Clear All Data warning#165
Bilb merged 24 commits into
mainfrom
test/pro-seeding-and-clear-data

Conversation

@Bilb

@Bilb Bilb commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #157, #159 and #164, which are closed in favour of this. #159 depended on #164, and all
three touch the same state-builder plumbing, so they conflict pairwise and are easier to review as one.

What is here

Seeded Pro, on any state (was #164). qa-seeder 0.3.0 → 0.4.1, where 1userWithProAccess is gone
and Pro is an option that composes with every state key. BuildStateOptions is threaded through both
state builders, and grantProToSeededUsers mints for whoever pro names — before linkDevices, since
the restore is the client's first sight of the account and a grant landing after it answers never.

Seeded pins above the client's limit (was #159). Only a seeded config can produce that state: the
client refuses the sixth pin itself, yet config carrying more arrives in production from a linked
device or a restore.

The Pro transfer warning on Clear All Data (was #157). 12 tests, the full deleteMode ×
useCurrentUserHasPro() grid on all three platforms. Every case reads the copy and cancels — the
destructive action is never taken.

Verification

Desktop Android iOS
Clear-data grid (12) ✅ 4/4 ✅ 4/4 ✅ 4/4
Pro granted to a seeded account
Pins above the limit from config n/a see below

Mobile was run against clients built from the merged qa/confirm-device-clear-for-all branches, not
dev.

The iOS pin spec fails, and it is not the spec's fault

Pinned conversations above the limit arriving from config fails on iOS with no CTA raised — three
consecutive runs, while the pre-existing Pinned conversation limit (non Pro) @ios passes in the same
session.

That is the case the spec was written to catch, and its comment predicted it:

Six is already past the limit, so this only holds if the client compares the pinned COUNT against the
limit rather than watching for the moment it is reached. An === STANDARD_PIN_LIMIT check passes
every existing pin spec — they all arrive at the limit one pin at a time — and lets this attempt
through.

Supporting evidence from the iOS source: pinnedConversationLimit has no enforcement call site. Its
only uses are the protocol declaration, a default, and interpolating {limit} into the CTA copy in
ProCTAModal.swift. Android handles the same state correctly.

So this needs a decision before merge: file the iOS bug and narrow that one spec to androidIt
until it is fixed, or hold the whole PR. I have not narrowed it unilaterally — a spec that fails
because it found something is worth more than a green one, and which way to resolve it is yours.

One fix made while verifying

The over-limit pin CTA uses a different token: proCallToActionPinnedConversations ("Want more
pins?"), not ...MoreThan ("Want more than 5 pins?"). The clients branch on it deliberately, and
telling someone holding six pins "want more than 5" would read as nonsense. #159 asserted the
at-the-limit copy and so failed on Android against correct behaviour. Added
pinnedConversationsOverLimit to the CTA table rather than loosening the existing entry — the spec's
second CTA check is at five pins, where the original copy is still right.

Merge conflicts resolved

#164 and #159 both added stateOptions plumbing independently. Unified on #164's With*-composed
argument types and on a single stateOptions, dropping #159's separate pins parameter —
stateOptions.pins already expresses it, and one mechanism means a future spec can ask for Pro and
pins together.

Bilb and others added 16 commits August 27, 2026 14:03
Pro-specific copy on an irreversible flow, read by nothing until now. It is the only thing telling
a subscriber their Pro does not survive the wipe, and it is a second-stage dialog: opening Clear
Data shows the generic copy, pressing Clear re-renders the same slot with the warning, and only a
further press deletes anything. Every case here reads the copy and cancels - the destructive action
is never taken.

Desktop gets all four cells of the grid the app actually branches on (`deleteMode` crossed with
`useCurrentUserHasPro()`). Each Pro case asserts two runs of copy because neither alone identifies a
cell: the transfer warning is word-for-word identical in both Pro tokens, and the network branch's
opening sentence is word-for-word `clearDeviceAndNetworkConfirm`, the standard copy.

Android gets the device branch only, and no standard-account control - `SettingsViewModel` calls
`clearDataDeviceOnly()` straight from the first Clear press for a standard account, so the control
would wipe the app rather than assert anything.

iOS gets nothing. See below.

`localizedRuns` is new and is the reason these assert runs rather than tokens. `tStripped` collapses
a `<br/>` to a single space while every client renders one as no character at all, so the stripped
string is a run of text that exists nowhere in the UI and can never match. Both Pro tokens span two
breaks. This splits the raw token on its breaks and returns the stripped runs, so the assertion
still comes from the localizer rather than a hardcoded English string.

Two client-side tag gaps this could not work around, both worth fixing:

- iOS has no test identifier anywhere in the flow. The settings row is a `ListItemInfo` built with
  no `accessibility:`, and `NukeDataModal` sets none on its radios, its buttons or its labels; the
  `ConfirmationModal` carrying the copy gets only an `accessibilityLabel`. Nothing here is
  addressable by identifier, so there is no iOS spec.
- Android's two clear-data radios are `RadioOption`s built with no `qaTag` in `SettingsScreen.kt`,
  so the network branch cannot be selected. Device-only is the default selection, which is the only
  reason that half is reachable.

The Android Clear/Cancel ids are the English display strings, because `AlertDialog` falls back to a
button's own text when the call site gives it no `qaTag`. Real ids rather than text matches, but
ones that would move with the locale - noted on the locators.

Verified: 4/4 desktop (12.0s), 1/1 android (30.1s).
qa-seeder removed that state key. Pro access is now an option addressed by a
user's index, so the same account is built as '1user' plus `{ pro: { 0: {} } }`
- empty terms being the seeder's defaults, 30 days out and auto-renewing, which
is exactly what the key granted.

`stateKey` reaches `buildStateForTest` through `openSeededWindows`, so the
options had to be threaded through it.

Needs qa-seeder 0.4.0: under 0.x a caret range confines ^0.3.0 to 0.3.x, and
this API is not in 0.3.0.
The clients enforce STANDARD_PIN_LIMIT and silently refuse the sixth pin, so an
account holding more cannot be built by tapping - yet it occurs in production,
because config arrives from a linked device or a restore and the pins may have
been made while the account was Pro elsewhere. The seeder writes the priorities
into config directly, which is the only way to reach the state the limit is
supposed to govern.

`open_Alice1_with_contacts` takes `pins` and returns `pinnedNames`, resolved
against the same user list the seeder pinned in, so no spec does index
arithmetic.

The new spec covers a never-Pro account arriving with six pins: all six honoured
and above the unpinned rest, a seventh refused, one unpinnable, and the freed
slot still unusable at five. One assertion - that the client honours the sixth
rather than clamping its display to five - could not be verified against an app
checkout and is marked in the file as needing a decision rather than being
weakened to pass.

Also adds a skipped stub for a message one character past the Pro limit, which
needs a seeder action that does not exist yet.
The clients now tag what this needed, so the mobile spec becomes `bothPlatformsIt` and gains the
network branch and a control.

iOS was previously unreachable: the settings row carried no identifier and neither did anything in
`NukeDataModal`. Android's two radios had no `qaTag`, so only the device branch - the default
selection - could be reached. Both are fixed in the client repos; nothing here works around them.

One thing that only shows up on a device. The two stages are the same dialog on Android, whose text
swaps, but two STACKED modals on iOS: the confirmation is presented over `NukeDataModal` rather than
replacing it, so both sit in the accessibility tree at once. Giving them the same identifiers had the
locator match whichever came first - the one underneath - and all three iOS specs failed asserting
the confirmation copy while reading the pre-confirmation copy. Hence `ClearDataDialogDescription` and
the iOS half of `ClearDataConfirmButton`, which name the first stage only.

Adds a standard-account control on the NETWORK branch. There cannot be one on the device branch:
both clients delete straight from the first Clear press there, with no confirmation to read.

Verified on all three: iOS 3/3 (31.5s, 31.7s, 31.1s), Android 3/3 (29.7s, 28.3s, 28.4s), Desktop 4/4
(18.2s). The iOS run needed a local build of the client change - the prebuilt app predates it.
An id says the client rendered the right control; only the copy says it rendered the right words in
it, and the two fail independently - a control keeps its identifier through a copy change, so an
id-only lookup stays green against a wrong, empty or swapped string. On a flow whose two actions are
Clear and Cancel that is the difference between reading a warning and wiping the account.

So every control this spec presses is now checked both ways, and CLAUDE.md carries the rule for
everything after it.

Where the copy lives is per-platform, and finding that out took a device:

- iOS puts it on `label`; an identifier becomes the element's `name` and displaces the display text.
- Desktop takes `text`, already a substring match.
- Android Compose CONTROLS report no text of their own - the label is a child node, so the node
  addressed by id has nothing to compare. Verified from a page-source dump: the Clear button resolves
  to a node with no text, while a sibling `TextView` carries "Clear". Only text-bearing nodes (a
  dialog body, a heading) can be checked in place.

`expectControlCopy` does that split and skips loudly on Android rather than passing quietly. Where an
Android id is itself derived from the display string - `AlertDialog` falls back to a button's own text
with no `qaTag` - the id lookup already covers the copy, and the comment says so.

Also corrects the standard-account note. Desktop confirms for every account on both branches; Android
and iOS confirm on both branches only for a PRO account, and send a standard account's device-only
clear straight to deletion. Confirmed on Android by running that case once - the app came back on the
onboarding screen. The iOS half of that is read from `clearDeviceOnly()`, not observed.

Verified with the assertions in place: iOS 3/3, Android 3/3, Desktop 4/4.
Two the previous commit wrote the rule for and then did not follow.

`ClearDataMenuItem` was tapped on its id alone, three call sites and no copy check anywhere - and it
is the case the rule exists for, since its id is a hand-written tag rather than the display string,
so the lookup says nothing about what the row reads.

`ClearDeviceOnlyRadio` was used zero times: added speculatively and never wired up. It now carries
its half of the first-stage assertion, so both branches the dialog offers are checked rather than
only the one a given case goes on to tap. The preselected radio is never pressed, so nothing else
would have read it.

Each locator's doc now says whether its id carries the copy, so the next caller does not have to
work it out: hand-written tag (the row), slug (the radios), or derived from the display string
(Android's dialog buttons, where the lookup already covers it).

Verified: iOS 3/3 - the platform these assertions actually bite on, since Android's Compose labels
are child nodes and `expectControlCopy` skips there.
0.4.0 is published but uninstallable: it depends on
@session-foundation/session-protos@^0.0.1, which was published, unpublished, and
so can never be republished - and a caret on a 0.0.x version pins that exact
patch, so nothing can satisfy it. 0.4.1 asks for ^0.0.2.
Drops the three standard-account cases, leaving two per platform.

The mobile one was written against behaviour that is being removed: both clients used to delete
straight from the first Clear press on the device branch, and now confirm for every account as
Desktop always has. Testing the old shape would have been testing something on its way out.

The two desktop ones were duplicates. `clearDataOnWindow` in `linked_device_group.spec.ts` already
walks the same modal on a standard account and asserts `clearDeviceDescription` on the confirmation,
via `checkModalStrings` - which is an exact match, so it also rules the Pro copy out.

What that costs, stated so it is a decision and not an oversight: nothing left here separates "shows
the Pro copy to Pro users" from "shows the Pro copy to everyone". Worth a control once the mobile
behaviour has settled, and cheap to add then.

Verified: iOS 2/2, Android 2/2, Desktop 2/2.
`1userWithProAccess` is gone in 0.4.1, replaced by a `pro` option that composes with every state. That
is the point of the bump: a Pro fixture no longer needs a state key of its own, so the twelve specs
built on `2friends` can have a Pro Alice without a `2friendsOneWithProAccess` existing.

`BuildStateOptions` is now forwarded from both state builders - `openAppsWithState` for mobile,
`openSeededWindows` for desktop - and `open_Alice1`, `open_Alice1_Bob1_friends` and
`open_Alice1_with_contacts` take it.

`grantProToSeededUsers` mints for whichever users `pro` names, and does it BEFORE `linkDevices`: the
restore is the client's first sight of the account, so the gated startup fetch fires as it completes.
A grant landing after that answers `never`, and both clients then floor the next attempt at 60s from
that attempt - which is what makes a spec need `observeProGrant` at all.

Still `makeAccountPro` rather than the seeder's own `fetchProProof`. That one verifies the proof
against the key the instance actually signs with, which is worth having and worth a follow-up, but
this keeps the guards against granting to the shared moderation account and against a phrase that
does not derive the account under test.

Argument types now compose from `With*` intersections like `WithPlatform` already did, rather than
repeating `testInfo`/`testContext`/`stateOptions` inline in nine signatures.

Verified: the seeded-grant spec passes on Android (31.8s), iOS (23.3s) and Desktop.

One environment note found on the way. The devnet has a dead snode - 1540 of the 15 it registers,
every other one up - and the seeder picks a random snode on the account's swarm with no failover, so
seeding fails outright when a new account's swarm happens to include it. Unrelated to this change: an
untouched desktop seeded spec fails the same way. Worth either reviving that snode or teaching the
seeder to try another.
`pressWithCopy` reinvented `clickOnWithText`, and the hand-rolled `waitForElement` on
`modal-description` reinvented `checkModalStrings` - which is the better of the two anyway, since it
scopes to `[data-modal-id="deleteAccountModal"]` and pins the heading at the same time. A bare
`modal-description` wait would be satisfied by any modal carrying that slot.

Both were sitting in `clearDataOnWindow` (`linked_device_group.spec.ts`) the whole time.

The consequence is the interesting part: `checkModalStrings` reads `innerText`, where a `<br/>`
renders as a newline, and then collapses whitespace - landing on exactly what `tStripped` produces for
the same token. So the `<br/>`-spanning problem is a MOBILE one, and Desktop can assert whole tokens.
`localizedRuns` is gone from this spec; keeping it would have been carrying a workaround for another
platform's constraint. Verified against `proClearAllDataDevice`, which spans two breaks.

Both cases still pass, and faster: 3.6s and 2.6s against 6.1s and 3.4s.

CLAUDE.md records both - reach for the desktop primitives, and the `<br/>` rule is mobile-only.
Adds the standard-account device case back, on all three platforms, now that it exists to test.

It could not before on mobile: a standard account pressing Clear with device-only selected had its
data deleted on that press - Android's `SettingsViewModel.clearData` fell through to
`clearDataDeviceOnly()`, iOS's `clearDeviceOnly()` to `clearLocalAccount()` - so there was no
confirmation to read and the test would have wiped the app instead of asserting. The client PRs change
that, and this is what stops it regressing to a one-tap wipe: reaching the assertion at all means the
confirmation was there.

It also closes the gap the previous commit deliberately left open. With Pro cases alone, nothing
separated "shows the Pro copy to Pro users" from "shows the Pro copy to everyone".

Device branch only. That is the branch the clients changed, and the same `useCurrentUserHasPro()`
drives both, so a network control would pin nothing new.

No existing spec needed updating: this is the only mobile spec that clears data, and the three desktop
ones that touch the flow (`delete_account`, `linked_device_group`, `enforce_localized_str`) sit on
behaviour that did not change.

Verified: iOS 3/3, Android 3/3, Desktop 3/3.
`localizedRuns` existed because a copy spanning a `<br/>` can never match a locator's text filter:
`tStripped` puts a single space where the break was, and the rendered value has a newline (mobile) or
no character at all (a DOM `textContent`). Splitting the token into runs was one way round that.

Collapsing the whitespace of the value being compared is a better one. `readDialogBody` now does it,
and the specs assert the WHOLE `tStripped` token - which pins more than a fragment did, since a
fragment could not say the two halves were in the right order or that anything sat between them.
Verified on both mobile platforms; Desktop already got this for free from `checkModalStrings`, which
reads `innerText` and normalises the same way.

So the helper is gone rather than mobile-only, and CLAUDE.md now describes the normalisation instead
of pointing at a file that no longer exists.

Also completes the grid. Each of the four cells - `deleteMode` crossed with `useCurrentUserHasPro()` -
carries its own token, so the standard-account NETWORK case is the only thing asserting the copy that
warns a standard user their messages cannot be restored. `delete_account.spec.ts` walks that branch
but never reads the confirmation. Previously left out on the grounds that it "pins nothing new", which
was wrong.

Verified: iOS 4/4, Android 4/4, Desktop 4/4.
Two things, both replacing hand-rolled work with harness machinery that already existed.

`readDialogBody` reimplemented the whitespace normalisation the matchers already do.
`findMatchingTextInElementArray` and `findMatchingLabelInElementArray` both collapse whitespace before
comparing, which is exactly what makes copy spanning a `<br/>` comparable to the single space
`tStripped` leaves in its place. So the locator's own `text`/`label` filter does the whole job, and
`withCopy` picks the right one per platform - `label` on iOS, where an identifier displaces the display
text; `text` on Android, where a text-bearing node still carries its own.

That match is EXACT rather than a substring, which is strictly stronger and removes the separate
"and not the Pro copy" assertions: a body that equals `clearDeviceDescription` cannot also contain the
transfer warning.

`withCopy` sits beside `expectControlCopy`, and the pair is the real distinction - a text-bearing node
can be checked on both platforms, a Compose control only on iOS.

Separately, `cancelClearData` was passing `PRESENT_MAX_WAIT` to `verifyElementNotPresent`, which sleeps
its `maxWait` UNCONDITIONALLY before looking. That was a flat ten seconds per case rather than a bound
a fast dismiss escapes. On ABSENT_MAX_WAIT the Android cases drop from ~29s to ~20s.

Verified against builds carrying the client behaviour change, not dev: iOS 4/4, Android 4/4.
# Conflicts:
#	run/test/state_builder/index.ts
An account already holding more pins than the limit gets `proCallToActionPinnedConversations` ("Want
more pins?"), not `proCallToActionPinnedConversationsMoreThan` ("Want more than 5 pins?"). The clients
branch on it deliberately - Android in `ProComponents.kt`, crossed with whether the plan has expired -
and telling someone holding six pins "want more than 5" would read as nonsense.

The seeded over-limit spec asserted the at-the-limit copy, so it failed on Android against correct
behaviour. Adds `pinnedConversationsOverLimit` to the CTA table rather than loosening the existing
entry: the two are different states, and the spec's second CTA check is at FIVE pins, where the
original copy is still right.
Comment thread run/test/specs/mobile/pro_pin_over_limit_from_config.spec.ts
Comment thread run/test/specs/mobile/pro_pin_over_limit_from_config.spec.ts Outdated
Comment thread run/test/specs/mobile/pro_over_pro_limit_message.spec.ts Outdated
Bilb added 5 commits August 28, 2026 08:57
…-seeder

The spec attempted its extra pin on `contactNames[6]` - the first UNPINNED row, which with six seeded
pins is already sitting in position 7. Pinning it moves it to the end of the pinned block, which is
also position 7, so the conversation order comes out identical whether the client honoured the pin or
refused it. `assertPinOrder` could not tell the two apart: the step read as a guard and asserted
nothing.

Pinning the LAST row instead moves it from tenth to seventh when allowed, so the order says which
happened. That matters most on iOS, where the CTA check cannot stand in - see below.

With that fixed the spec immediately found what it was written for: **iOS allows the seventh pin**,
leaving a standard account holding seven against a limit of five. Kevin moves from tenth to seventh.
Android refuses it and raises the CTA. Both symptoms have one cause, so the CTA assertion stays on
both platforms rather than being gated to Android - gating it would hide a real defect behind a green
run.

Also bumps qa-seeder 0.4.1 -> 0.6.0, which adds `grantProPayment` (payment on record, with
`durationSeconds`, the shared-admin refusal and a granted-duration check) and `sendProMessage`. Both
are what `pro_over_pro_limit_message` has been waiting on. No breaking changes for existing callers.
The marker had no identifier on iOS, so `ConversationPinnedIcon` threw for it and
every pin-icon assertion was gated behind `.onAndroid()`. session-ios #767 tags
it, carrying the conversation name on the label the way the cell already does,
so all four call sites now run on both platforms.

That immediately caught two things.

The pin marker is what proves an over-limit config renders as it is rather than
being clamped to five — before this, the iOS half of
`pro_pin_over_limit_from_config` rested entirely on the conversation order, which
cannot tell a hidden marker from an absent one.

And `findElementQuietly` ignored a locator's `label`, so `waitForElementToBeGone`
matched any element sharing the id. Unpinning one of six conversations left five
markers behind and the wait could never pass. The "gone" path now narrows the
same way the "present" path does.

Also drops the note claiming iOS pins the seventh conversation. It does, but only
without session-ios #762 plus #767 — the comment now says which build that is
rather than reading as settled product behaviour.
Every existing character-limit spec is written from the sender's side, because
that is the only side a client can reach: the composer refuses past the limit. So
the receiving half of the rule was untested — accept it whole, cut it at the Pro
limit, fall back to the standard limit, or drop it were all equally consistent
with a green suite.

qa-seeder 0.6.0's `sendProMessage` manufactures the message, consulting no limit
and carrying a real backend-signed proof. The sender has to be genuinely Pro: a
recipient verifies the proof before honouring anything past the standard limit,
so a mocked one would be measuring the wrong refusal.

Length is pinned by four probes rather than read, because neither platform
exposes it — `boundary` ends at exactly PRO_MAX_CHARS and `overflow` is that plus
the one character past it, so only a body of exactly PRO_MAX_CHARS satisfies
both. `early` and `late` straddle the standard limit, separating "never arrived"
and "proof not honoured" from "cut in the wrong place". Each outcome throws its
own message.

Bob gets no device or window: he is an account the seeder acts as, so
`open_Alice1_Bob0_friends` and `test_Alice_1W_Bob_0W_friends` seed the friendship
and open one side.

Verified on all three: iOS 19s, Android 22s, Desktop 8s. Desktop's assertions were
mutation-checked against a marker that cannot exist, since a fast pass with no
output is indistinguishable from a vacuous one.

Plain ASCII deliberately. Android and Desktop truncate by code point, iOS by
UTF-16 unit, so a non-BMP body lands at 10,000 on two clients and 5,000 on the
third. That divergence wants its own spec and a per-platform expectation.
Proofs live a real ~30 days, so nothing lapses on its own inside a run and a
spec written to wait for one waits forever. A short-lived entitlement now has to
be asked for, via the seeder's `durationSeconds`.

Observed as ~2.53M seconds remaining on a freshly minted proof, against the ~290s
the runbook promised.
session-ios #767 moved the conversation name from the marker's accessibility
label to its identifier, on review: a label is read aloud, and the screen reader
has just announced the same name from the cell. The marker is no longer an
accessibility element at all, so it is now invisible to VoiceOver rather than
merely less repetitive.

The selector is therefore per-conversation, which also means the `label` filter
is no longer what narrows it. The `findElementQuietly` fix in the previous commit
stays: `waitForElementToBeGone` ignoring a locator's `label` is a real asymmetry
with the "present" path, it just is not this locator's problem any more.
Comment thread run/test/state_builder/index.ts
Bilb added 2 commits August 28, 2026 11:53
`markedMessage` and `overProLimitMessage` were the same three steps written
twice: pad to an offset, drop a marker, repeat, then re-read the result to prove
the offsets came out right. Only the markers and the total length differed.

`bodyWithMarkers` takes the markers and the length and does the rest. It also
proves more than either did: that no marker overlaps the one before it, that they
fit inside the body, and that each occurs exactly ONCE — a marker appearing twice
makes a probe unable to say where the copy was cut, which neither version checked.

The EARLY/LATE pair is shared rather than repeated, since both bodies want it for
the same reason and the straddle only holds for tags short enough to leave EARLY
inside the standard limit.

Verified behaviour-preserving: both functions emit byte-identical bodies to the
previous implementation across five tags including a 33-character one, and the
three specs that consume them pass.

Also fixes two lint errors that were on the branch, one of which I reported as
clean off a grep that had matched a filename.
The mobile and desktop suites carried the same partition-and-compare
assertion twice, and only the mobile copy documented the trap that makes
it toothless: pinning a prefix of `beforeOrder` expects `beforeOrder`
back, so the assertion cannot fail. One copy now lives in
`run/shared/conversation_order.ts` with that note, following the
`assertUrlIsReachable` pattern; both suites re-export it, so no call site
moved. The two `getConversationOrder` readers stay per-platform — one
takes a Playwright `Page`, the other a `DeviceWrapper`.

`verify` moves to `run/shared/verify.ts` alongside it. It depends on
nothing but Playwright, so keeping it in the mobile grab-bag meant a
desktop import would drag `appium-xcuitest-driver`, `@wdio/types` and
the adb helpers into the process for a 30-line wrapper.

`assertPinOrder` deliberately stays on plain `expect`: `verify` replaces
Playwright's diff with a comma-joined `String()`, which reads well for
the scalar comparisons it is used for and loses exactly what an order
failure needs — which two names swapped.
The mobile copy used `verify` before the merge, and the only reason the
shared one did not was that `verify` lived in the mobile grab-bag. It no
longer does, so restore the failure output the mobile suite had — and
give the desktop suite the same clean Allure message.

The cost is a `.toEqual` over arrays printing both orders comma-joined
rather than as a diff. Both lists print in full, so a swap is still
readable.
@Bilb
Bilb merged commit b357863 into main Aug 28, 2026
1 check passed
@Bilb
Bilb deleted the test/pro-seeding-and-clear-data branch August 28, 2026 05:28
mpretty-cyro added a commit that referenced this pull request Aug 28, 2026
#165 landed the iOS pinned-marker locator and a second pinned-conversation
CTA, both of which this branch had reached independently.

- `ConversationPinnedIcon` for iOS is the same selector on both sides; kept
  main's comment, and carried over the note that a composite id is matched
  rather than an id paired with text, which breaks once an id is added.
- The CTA table now holds three pinned-conversation entries, not two: at the
  limit, already over it, and over it having previously subscribed. They are
  three of the four cells the clients pick from, and none substitutes for
  another.
- In the pin loops, main's removal of the `.onAndroid()` gate stands (the iOS
  locator exists now) and so does this branch's removal of the explicit
  `PlusButton` wait, which moved inside `pinConversation`.
mpretty-cyro added a commit that referenced this pull request Aug 28, 2026
Brings in main through #160: the iOS pinned-marker locator, the third
pinned-conversation CTA, and the qa-seeder bump. Nothing here overlaps with
the picker work — the only shared file is `DeviceWrapper`, and the two
changes sit in different methods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants