diff --git a/.gitattributes b/.gitattributes index 99ac5c82c8..64b88312a8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ -run/screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/appium_next.d.ts b/appium_next.d.ts deleted file mode 100644 index bed033dc9e..0000000000 --- a/appium_next.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-object-type */ -import { ExternalDriver } from '@appium/types/build/lib/driver'; - -// typings comes from : -// node_modules/@appium/types/build/lib/driver.d.ts -// BUT. they are defined as optional, so here we just copy and paste the one we need, and hardcode the fact that they are defined. -// We need to do this, because the iosDriver and the androidDriver do not export the typings (where they defined that those function exists) - -export interface MightBeUndefinedDeviceType extends ExternalDriver {} - -export type AppiumNextDeviceType = { - pushFile(remotePath: string, payloadBase64: string): Promise; - - // not sure at all - touchMove(x: number, y: number): Promise; -}; diff --git a/english_wordlist.txt b/english_wordlist.txt new file mode 100644 index 0000000000..9b92e9344e --- /dev/null +++ b/english_wordlist.txt @@ -0,0 +1,1626 @@ +abbey +abducts +ability +ablaze +abnormal +abort +abrasive +absorb +abyss +academy +aces +aching +acidic +acoustic +acquire +across +actress +acumen +adapt +addicted +adept +adhesive +adjust +adopt +adrenalin +adult +adventure +aerial +afar +affair +afield +afloat +afoot +afraid +after +against +agenda +aggravate +agile +aglow +agnostic +agony +agreed +ahead +aided +ailments +aimless +airport +aisle +ajar +akin +alarms +album +alchemy +alerts +algebra +alkaline +alley +almost +aloof +alpine +already +also +altitude +alumni +always +amaze +ambush +amended +amidst +ammo +amnesty +among +amply +amused +anchor +android +anecdote +angled +ankle +annoyed +answers +antics +anvil +anxiety +anybody +apart +apex +aphid +aplomb +apology +apply +apricot +aptitude +aquarium +arbitrary +archer +ardent +arena +argue +arises +army +around +arrow +arsenic +artistic +ascend +ashtray +aside +asked +asleep +aspire +assorted +asylum +athlete +atlas +atom +atrium +attire +auburn +auctions +audio +august +aunt +austere +autumn +avatar +avidly +avoid +awakened +awesome +awful +awkward +awning +awoken +axes +axis +axle +aztec +azure +baby +bacon +badge +baffles +bagpipe +bailed +bakery +balding +bamboo +banjo +baptism +basin +batch +bawled +bays +because +beer +befit +begun +behind +being +below +bemused +benches +berries +bested +betting +bevel +beware +beyond +bias +bicycle +bids +bifocals +biggest +bikini +bimonthly +binocular +biology +biplane +birth +biscuit +bite +biweekly +blender +blip +bluntly +boat +bobsled +bodies +bogeys +boil +boldly +bomb +border +boss +both +bounced +bovine +bowling +boxes +boyfriend +broken +brunt +bubble +buckets +budget +buffet +bugs +building +bulb +bumper +bunch +business +butter +buying +buzzer +bygones +byline +bypass +cabin +cactus +cadets +cafe +cage +cajun +cake +calamity +camp +candy +casket +catch +cause +cavernous +cease +cedar +ceiling +cell +cement +cent +certain +chlorine +chrome +cider +cigar +cinema +circle +cistern +citadel +civilian +claim +click +clue +coal +cobra +cocoa +code +coexist +coffee +cogs +cohesive +coils +colony +comb +cool +copy +corrode +costume +cottage +cousin +cowl +criminal +cube +cucumber +cuddled +cuffs +cuisine +cunning +cupcake +custom +cycling +cylinder +cynical +dabbing +dads +daft +dagger +daily +damp +dangerous +dapper +darted +dash +dating +dauntless +dawn +daytime +dazed +debut +decay +dedicated +deepest +deftly +degrees +dehydrate +deity +dejected +delayed +demonstrate +dented +deodorant +depth +desk +devoid +dewdrop +dexterity +dialect +dice +diet +different +digit +dilute +dime +dinner +diode +diplomat +directed +distance +ditch +divers +dizzy +doctor +dodge +does +dogs +doing +dolphin +domestic +donuts +doorway +dormant +dosage +dotted +double +dove +down +dozen +dreams +drinks +drowning +drunk +drying +dual +dubbed +duckling +dude +duets +duke +dullness +dummy +dunes +duplex +duration +dusted +duties +dwarf +dwelt +dwindling +dying +dynamite +dyslexic +each +eagle +earth +easy +eating +eavesdrop +eccentric +echo +eclipse +economics +ecstatic +eden +edgy +edited +educated +eels +efficient +eggs +egotistic +eight +either +eject +elapse +elbow +eldest +eleven +elite +elope +else +eluded +emails +ember +emerge +emit +emotion +empty +emulate +energy +enforce +enhanced +enigma +enjoy +enlist +enmity +enough +enraged +ensign +entrance +envy +epoxy +equip +erase +erected +erosion +error +eskimos +espionage +essential +estate +etched +eternal +ethics +etiquette +evaluate +evenings +evicted +evolved +examine +excess +exhale +exit +exotic +exquisite +extra +exult +fabrics +factual +fading +fainted +faked +fall +family +fancy +farming +fatal +faulty +fawns +faxed +fazed +feast +february +federal +feel +feline +females +fences +ferry +festival +fetches +fever +fewest +fiat +fibula +fictional +fidget +fierce +fifteen +fight +films +firm +fishing +fitting +five +fixate +fizzle +fleet +flippant +flying +foamy +focus +foes +foggy +foiled +folding +fonts +foolish +fossil +fountain +fowls +foxes +foyer +framed +friendly +frown +fruit +frying +fudge +fuel +fugitive +fully +fuming +fungal +furnished +fuselage +future +fuzzy +gables +gadget +gags +gained +galaxy +gambit +gang +gasp +gather +gauze +gave +gawk +gaze +gearbox +gecko +geek +gels +gemstone +general +geometry +germs +gesture +getting +geyser +ghetto +ghost +giant +giddy +gifts +gigantic +gills +gimmick +ginger +girth +giving +glass +gleeful +glide +gnaw +gnome +goat +goblet +godfather +goes +goggles +going +goldfish +gone +goodbye +gopher +gorilla +gossip +gotten +gourmet +governing +gown +greater +grunt +guarded +guest +guide +gulp +gumball +guru +gusts +gutter +guys +gymnast +gypsy +gyrate +habitat +hacksaw +haggled +hairy +hamburger +happens +hashing +hatchet +haunted +having +hawk +haystack +hazard +hectare +hedgehog +heels +hefty +height +hemlock +hence +heron +hesitate +hexagon +hickory +hiding +highway +hijack +hiker +hills +himself +hinder +hippo +hire +history +hitched +hive +hoax +hobby +hockey +hoisting +hold +honked +hookup +hope +hornet +hospital +hotel +hounded +hover +howls +hubcaps +huddle +huge +hull +humid +hunter +hurried +husband +huts +hybrid +hydrogen +hyper +iceberg +icing +icon +identity +idiom +idled +idols +igloo +ignore +iguana +illness +imagine +imbalance +imitate +impel +inactive +inbound +incur +industrial +inexact +inflamed +ingested +initiate +injury +inkling +inline +inmate +innocent +inorganic +input +inquest +inroads +insult +intended +inundate +invoke +inwardly +ionic +irate +iris +irony +irritate +island +isolated +issued +italics +itches +items +itinerary +itself +ivory +jabbed +jackets +jaded +jagged +jailed +jamming +january +jargon +jaunt +javelin +jaws +jazz +jeans +jeers +jellyfish +jeopardy +jerseys +jester +jetting +jewels +jigsaw +jingle +jittery +jive +jobs +jockey +jogger +joining +joking +jolted +jostle +journal +joyous +jubilee +judge +juggled +juicy +jukebox +july +jump +junk +jury +justice +juvenile +kangaroo +karate +keep +kennel +kept +kernels +kettle +keyboard +kickoff +kidneys +king +kiosk +kisses +kitchens +kiwi +knapsack +knee +knife +knowledge +knuckle +koala +laboratory +ladder +lagoon +lair +lakes +lamb +language +laptop +large +last +later +launching +lava +lawsuit +layout +lazy +lectures +ledge +leech +left +legion +leisure +lemon +lending +leopard +lesson +lettuce +lexicon +liar +library +licks +lids +lied +lifestyle +light +likewise +lilac +limits +linen +lion +lipstick +liquid +listen +lively +loaded +lobster +locker +lodge +lofty +logic +loincloth +long +looking +lopped +lordship +losing +lottery +loudly +love +lower +loyal +lucky +luggage +lukewarm +lullaby +lumber +lunar +lurk +lush +luxury +lymph +lynx +lyrics +macro +madness +magically +mailed +major +makeup +malady +mammal +maps +masterful +match +maul +maverick +maximum +mayor +maze +meant +mechanic +medicate +meeting +megabyte +melting +memoir +menu +merger +mesh +metro +mews +mice +midst +mighty +mime +mirror +misery +mittens +mixture +moat +mobile +mocked +mohawk +moisture +molten +moment +money +moon +mops +morsel +mostly +motherly +mouth +movement +mowing +much +muddy +muffin +mugged +mullet +mumble +mundane +muppet +mural +musical +muzzle +myriad +mystery +myth +nabbing +nagged +nail +names +nanny +napkin +narrate +nasty +natural +nautical +navy +nearby +necklace +needed +negative +neither +neon +nephew +nerves +nestle +network +neutral +never +newt +nexus +nibs +niche +niece +nifty +nightly +nimbly +nineteen +nirvana +nitrogen +nobody +nocturnal +nodes +noises +nomad +noodles +northern +nostril +noted +nouns +novelty +nowhere +nozzle +nuance +nucleus +nudged +nugget +nuisance +null +number +nuns +nurse +nutshell +nylon +oaks +oars +oasis +oatmeal +obedient +object +obliged +obnoxious +observant +obtains +obvious +occur +ocean +october +odds +odometer +offend +often +oilfield +ointment +okay +older +olive +olympics +omega +omission +omnibus +onboard +oncoming +oneself +ongoing +onion +online +onslaught +onto +onward +oozed +opacity +opened +opposite +optical +opus +orange +orbit +orchid +orders +organs +origin +ornament +orphans +oscar +ostrich +otherwise +otter +ouch +ought +ounce +ourselves +oust +outbreak +oval +oven +owed +owls +owner +oxidant +oxygen +oyster +ozone +pact +paddles +pager +pairing +palace +pamphlet +pancakes +paper +paradise +pastry +patio +pause +pavements +pawnshop +payment +peaches +pebbles +peculiar +pedantic +peeled +pegs +pelican +pencil +people +pepper +perfect +pests +petals +phase +pheasants +phone +phrases +physics +piano +picked +pierce +pigment +piloted +pimple +pinched +pioneer +pipeline +pirate +pistons +pitched +pivot +pixels +pizza +playful +pledge +pliers +plotting +plus +plywood +poaching +pockets +podcast +poetry +point +poker +polar +ponies +pool +popular +portents +possible +potato +pouch +poverty +powder +pram +present +pride +problems +pruned +prying +psychic +public +puck +puddle +puffin +pulp +pumpkins +punch +puppy +purged +push +putty +puzzled +pylons +pyramid +python +queen +quick +quote +rabbits +racetrack +radar +rafts +rage +railway +raking +rally +ramped +randomly +rapid +rarest +rash +rated +ravine +rays +razor +react +rebel +recipe +reduce +reef +refer +regular +reheat +reinvest +rejoices +rekindle +relic +remedy +renting +reorder +repent +request +reruns +rest +return +reunion +revamp +rewind +rhino +rhythm +ribbon +richly +ridges +rift +rigid +rims +ringing +riots +ripped +rising +ritual +river +roared +robot +rockets +rodent +rogue +roles +romance +roomy +roped +roster +rotate +rounded +rover +rowboat +royal +ruby +rudely +ruffled +rugged +ruined +ruling +rumble +runway +rural +rustled +ruthless +sabotage +sack +sadness +safety +saga +sailor +sake +salads +sample +sanity +sapling +sarcasm +sash +satin +saucepan +saved +sawmill +saxophone +sayings +scamper +scenic +school +science +scoop +scrub +scuba +seasons +second +sedan +seeded +segments +seismic +selfish +semifinal +sensible +september +sequence +serving +session +setup +seventh +sewage +shackles +shelter +shipped +shocking +shrugged +shuffled +shyness +siblings +sickness +sidekick +sieve +sifting +sighting +silk +simplest +sincerely +sipped +siren +situated +sixteen +sizes +skater +skew +skirting +skulls +skydive +slackens +sleepless +slid +slower +slug +smash +smelting +smidgen +smog +smuggled +snake +sneeze +sniff +snout +snug +soapy +sober +soccer +soda +software +soggy +soil +solved +somewhere +sonic +soothe +soprano +sorry +southern +sovereign +sowed +soya +space +speedy +sphere +spiders +splendid +spout +sprig +spud +spying +square +stacking +stellar +stick +stockpile +strained +stunning +stylishly +subtly +succeed +suddenly +suede +suffice +sugar +suitcase +sulking +summon +sunken +superior +surfer +sushi +suture +swagger +swept +swiftly +sword +swung +syllabus +symptoms +syndrome +syringe +system +taboo +tacit +tadpoles +tagged +tail +taken +talent +tamper +tanks +tapestry +tarnished +tasked +tattoo +taunts +tavern +tawny +taxi +teardrop +technical +tedious +teeming +tell +template +tender +tepid +tequila +terminal +testing +tether +textbook +thaw +theatrics +thirsty +thorn +threaten +thumbs +thwart +ticket +tidy +tiers +tiger +tilt +timber +tinted +tipsy +tirade +tissue +titans +toaster +tobacco +today +toenail +toffee +together +toilet +token +tolerant +tomorrow +tonic +toolbox +topic +torch +tossed +total +touchy +towel +toxic +toyed +trash +trendy +tribal +trolling +truth +trying +tsunami +tubes +tucks +tudor +tuesday +tufts +tugs +tuition +tulips +tumbling +tunnel +turnip +tusks +tutor +tuxedo +twang +tweezers +twice +twofold +tycoon +typist +tyrant +ugly +ulcers +ultimate +umbrella +umpire +unafraid +unbending +uncle +under +uneven +unfit +ungainly +unhappy +union +unjustly +unknown +unlikely +unmask +unnoticed +unopened +unplugs +unquoted +unrest +unsafe +until +unusual +unveil +unwind +unzip +upbeat +upcoming +update +upgrade +uphill +upkeep +upload +upon +upper +upright +upstairs +uptight +upwards +urban +urchins +urgent +usage +useful +usher +using +usual +utensils +utility +utmost +utopia +uttered +vacation +vague +vain +value +vampire +vane +vapidly +vary +vastness +vats +vaults +vector +veered +vegan +vehicle +vein +velvet +venomous +verification +vessel +veteran +vexed +vials +vibrate +victim +video +viewpoint +vigilant +viking +village +vinegar +violin +vipers +virtual +visited +vitals +vivid +vixen +vocal +vogue +voice +volcano +vortex +voted +voucher +vowels +voyage +vulture +wade +waffle +wagtail +waist +waking +wallets +wanted +warped +washing +water +waveform +waxing +wayside +weavers +website +wedge +weekday +weird +welders +went +wept +were +western +wetsuit +whale +when +whipped +whole +wickets +width +wield +wife +wiggle +wildly +winter +wipeout +wiring +wise +withdrawn +wives +wizard +wobbly +woes +woken +wolf +womanly +wonders +woozy +worry +wounded +woven +wrap +wrist +wrong +yacht +yahoo +yanks +yard +yawning +yearbook +yellow +yesterday +yeti +yields +yodel +yoga +younger +yoyo +zapped +zeal +zebra +zero +zesty +zigzags +zinger +zippers +zodiac +zombie +zones +zoom \ No newline at end of file diff --git a/package.json b/package.json index 1b0ff29028..7b4aa28d04 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ }, "dependencies": { "@appium/support": "^7.0.5", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", "@playwright/test": "^1.58.2", "@session-foundation/playwright-reporter": "^0.0.8", "@session-foundation/qa-seeder": "^0.1.26", @@ -78,7 +80,8 @@ "packageManager": "pnpm@10.28.1", "pnpm": { "patchedDependencies": { - "appium-uiautomator2-driver": "patches/appium-uiautomator2-driver.patch" + "appium-uiautomator2-driver": "patches/appium-uiautomator2-driver.patch", + "allure-playwright@3.4.5": "patches/allure-playwright@3.4.5.patch" }, "ignoredBuiltDependencies": [ "appium-ios-tuntap", diff --git a/patches/allure-playwright@3.4.5.patch b/patches/allure-playwright@3.4.5.patch new file mode 100644 index 0000000000..41c3e62670 --- /dev/null +++ b/patches/allure-playwright@3.4.5.patch @@ -0,0 +1,38 @@ +diff --git a/dist/cjs/index.js b/dist/cjs/index.js +index e09edfdc7f8ddc07f8865a9df6a8895b62f7998c..8af3d392a9a70b971f8fdb0f88e4ed336c5719e0 100644 +--- a/dist/cjs/index.js ++++ b/dist/cjs/index.js +@@ -504,12 +504,12 @@ var AllureReporter = exports.AllureReporter = /*#__PURE__*/function () { + testResult.stage = _allureJsCommons.Stage.FINISHED; + }); + if (result.stdout.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stdout", Buffer.from((0, _sdk.stripAnsi)(result.stdout.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner logs", Buffer.from((0, _sdk.stripAnsi)(result.stdout.join("")), "utf-8"), { + contentType: _allureJsCommons.ContentType.TEXT + }); + } + if (result.stderr.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stderr", Buffer.from((0, _sdk.stripAnsi)(result.stderr.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner errors", Buffer.from((0, _sdk.stripAnsi)(result.stderr.join("")), "utf-8"), { + contentType: _allureJsCommons.ContentType.TEXT + }); + } +diff --git a/dist/esm/index.js b/dist/esm/index.js +index 84ab12679c08c2738e7d9fb53267f950c60cfb86..b55ef16e6900f444aee1944cfa286baa37e6369c 100644 +--- a/dist/esm/index.js ++++ b/dist/esm/index.js +@@ -486,12 +486,12 @@ export var AllureReporter = /*#__PURE__*/function () { + testResult.stage = Stage.FINISHED; + }); + if (result.stdout.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stdout", Buffer.from(stripAnsi(result.stdout.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner logs", Buffer.from(stripAnsi(result.stdout.join("")), "utf-8"), { + contentType: ContentType.TEXT + }); + } + if (result.stderr.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stderr", Buffer.from(stripAnsi(result.stderr.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner errors", Buffer.from(stripAnsi(result.stderr.join("")), "utf-8"), { + contentType: ContentType.TEXT + }); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 165f2e3521..ce24ef4f2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ overrides: tar-fs@>=3.0.0 <3.0.7: '>=3.0.7' patchedDependencies: + allure-playwright@3.4.5: + hash: 8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305 + path: patches/allure-playwright@3.4.5.patch appium-uiautomator2-driver: hash: 8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121 path: patches/appium-uiautomator2-driver.patch @@ -22,6 +25,12 @@ importers: '@appium/support': specifier: ^7.0.5 version: 7.0.5 + '@noble/curves': + specifier: ^2.0.1 + version: 2.0.1 + '@noble/hashes': + specifier: ^2.0.1 + version: 2.0.1 '@playwright/test': specifier: ^1.58.2 version: 1.58.2 @@ -88,10 +97,10 @@ importers: version: 2.36.0 allure-js-commons: specifier: ^3.4.5 - version: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)) + version: 3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)) allure-playwright: specifier: ^3.4.5 - version: 3.4.5(@playwright/test@1.58.2) + version: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) eslint: specifier: ^10.0.0 version: 10.0.0 @@ -447,6 +456,14 @@ packages: '@jsquash/png@3.1.1': resolution: {integrity: sha512-C10pc+0H6j0h8fENOfnGOvkXCmvpSQTDGlfGd0sHphZhPSGTyLjIrHba0FaZZdsKqA/wlmhYicUHb92vfZphaw==} + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3400,6 +3417,12 @@ snapshots: '@jsquash/png@3.1.1': {} + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3766,16 +3789,16 @@ snapshots: allure-commandline@2.36.0: {} - allure-js-commons@3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)): + allure-js-commons@3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)): dependencies: md5: 2.3.0 optionalDependencies: - allure-playwright: 3.4.5(@playwright/test@1.58.2) + allure-playwright: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) - allure-playwright@3.4.5(@playwright/test@1.58.2): + allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2): dependencies: '@playwright/test': 1.58.2 - allure-js-commons: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)) + allure-js-commons: 3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)) ansi-regex@5.0.1: {} @@ -5627,7 +5650,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 diff --git a/run/constants/community.ts b/run/constants/community.ts index 5a575110c5..3904cc81d7 100644 --- a/run/constants/community.ts +++ b/run/constants/community.ts @@ -1,3 +1,21 @@ -export const testCommunityLink = `https://chat.lokinet.dev/testing-all-the-things?public_key=1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17`; -export const testCommunityName = `Testing All The Things!`; -export const unresolvedTestcommunityName = 'testing-all-the-things'; +type CommunityConfig = { + link: string; + name: string; + roomName?: string; +}; + +export const communities: Record = { + testCommunity: { + link: 'https://test-chat.session.codes/testing-all-the-things?public_key=1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17', + name: 'Testing All The Things!', + roomName: 'testing-all-the-things', + }, + lokinetUpdates: { + link: 'https://open.getsession.org/lokinet-updates?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', + name: 'Lokinet Updates', + }, + sessionNetworkUpdates: { + link: 'https://open.getsession.org/oxen-updates?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', + name: 'Session Network Updates', + }, +}; diff --git a/run/constants/index.ts b/run/constants/index.ts index 1e9ba83a08..504d6b9104 100644 --- a/run/constants/index.ts +++ b/run/constants/index.ts @@ -22,6 +22,8 @@ export const testLink = `https://getsession.org/`; export const DEVNET_URL = 'http://sesh-net.local:1280'; +export const PRO_BACKEND_URL = 'https://pro-backend-dev.getsession.org'; + export const ONS_MAPPINGS = { TESTQA: { ons: 'testqa', diff --git a/run/constants/testfiles.ts b/run/constants/testfiles.ts index 8f66e8cd7a..ba5d230b61 100644 --- a/run/constants/testfiles.ts +++ b/run/constants/testfiles.ts @@ -3,3 +3,4 @@ export const testFile = 'test_file.pdf'; export const testVideo = 'test_video.mp4'; export const testVideoThumbnail = 'test_video_thumbnail.png'; export const profilePicture = 'profile_picture.jpg'; +export const animatedProfilePicture = 'animated_profile_picture.gif'; diff --git a/run/localizer/englishStrippedStr.ts b/run/localizer/englishStrippedStr.ts deleted file mode 100644 index e90253adac..0000000000 --- a/run/localizer/englishStrippedStr.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LocalizedStringBuilder, MergedLocalizerTokens } from "./lib"; - -export function englishStrippedStr(token: T) { - const builder = new LocalizedStringBuilder(token).stripIt().forceEnglish(); - return builder; -} diff --git a/run/screenshots/android/cta_donate.png b/run/screenshots/android/cta_donate.png index 4902856ebe..704211b7c5 100644 --- a/run/screenshots/android/cta_donate.png +++ b/run/screenshots/android/cta_donate.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b5561d790a9d73b1afbf9c61262ce89b5e1cba85cecb8f445cd9634876943e4 -size 1146466 +oid sha256:3975c78efa3c6c09a91fc78f8d82db5d4d48a2fe1c2f11c681f11bb4c03eef07 +size 1115997 diff --git a/run/screenshots/android/cta_pro_activated.png b/run/screenshots/android/cta_pro_activated.png new file mode 100644 index 0000000000..c47bd68391 --- /dev/null +++ b/run/screenshots/android/cta_pro_activated.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35fbdb49f97531ef2cb25b3027846d03ec4dc3590a02be30057770da80db8a27 +size 428879 diff --git a/run/screenshots/android/landingpage_new_account.png b/run/screenshots/android/landingpage_new_account.png index 276e5373b9..9a40983b42 100644 --- a/run/screenshots/android/landingpage_new_account.png +++ b/run/screenshots/android/landingpage_new_account.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cceb03631b9144e157f1c068f5917f58042c567adea70f3a9d74a9a478d9f02 -size 136014 +oid sha256:f0dcb7982a90edb0ea6b5de654429c670073dff99be415d9499d33bcacfc6868 +size 101390 diff --git a/run/screenshots/android/settings_conversations.png b/run/screenshots/android/settings_conversations.png index 983c94eab7..45534e20b8 100644 --- a/run/screenshots/android/settings_conversations.png +++ b/run/screenshots/android/settings_conversations.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:183f7aac856c61eb5f46638fc25dbb1755215712752b50f721698f3301a232d8 -size 156289 +oid sha256:c92c63b479821ffda45eba99884e4bd2cfa6fbe77e8d188508c43d9de35dfbfa +size 151360 diff --git a/run/screenshots/android/settings_notifications.png b/run/screenshots/android/settings_notifications.png index 0d9ee91e38..6229c00a96 100644 --- a/run/screenshots/android/settings_notifications.png +++ b/run/screenshots/android/settings_notifications.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e26cf860dbfb83a797c7465c826098601d275d15a4c69ee2dfe083d790661bfb -size 154470 +oid sha256:aec60ca8cc003a4d5e906a0b4c6a28e2b31c64ebfb5a601c998d5c22d3727c18 +size 147698 diff --git a/run/screenshots/android/settings_privacy.png b/run/screenshots/android/settings_privacy.png index 90b58f6b96..7da23a6d85 100644 --- a/run/screenshots/android/settings_privacy.png +++ b/run/screenshots/android/settings_privacy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e37c1174f4651cf2adb148517fe86ae36a6006bc50b71c9f2a1a329ac38f2940 -size 197778 +oid sha256:a464b633f442efc4a4515e9e6b52222de79b5a1ecc08c37966ab1df8eff1017d +size 209443 diff --git a/run/test/locators/conversation.ts b/run/test/locators/conversation.ts index 2adeca44be..f132ce34d2 100644 --- a/run/test/locators/conversation.ts +++ b/run/test/locators/conversation.ts @@ -1,6 +1,6 @@ import type { DeviceWrapper } from '../../types/DeviceWrapper'; -import { testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { StrategyExtractionObj } from '../../types/testing'; import { getAppDisplayName } from '../utils/devnet'; @@ -64,13 +64,13 @@ export class CommunityInvitation extends LocatorsInterface { return { strategy: 'id', selector: 'network.loki.messenger:id/openGroupTitleTextView', - text: testCommunityName, + text: communities.testCommunity.name, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Community invitation', - text: testCommunityName, + text: communities.testCommunity.name, } as const; } } diff --git a/run/test/locators/global.ts b/run/test/locators/global.ts index 87c991c6e9..58d264af47 100644 --- a/run/test/locators/global.ts +++ b/run/test/locators/global.ts @@ -100,7 +100,7 @@ export class CopyURLButton extends LocatorsInterface { } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) // See SES-4930 export class CTABody extends LocatorsInterface { public build() { @@ -112,15 +112,13 @@ export class CTABody extends LocatorsInterface { } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, + strategy: 'accessibility id', + selector: 'cta-body', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA -// See SES-4930 export class CTAButtonNegative extends LocatorsInterface { public build() { switch (this.platform) { @@ -133,14 +131,12 @@ export class CTAButtonNegative extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Maybe Later', + selector: 'cta-button-negative', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA -// See SES-4930 export class CTAButtonPositive extends LocatorsInterface { public build() { switch (this.platform) { @@ -153,14 +149,12 @@ export class CTAButtonPositive extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Donate', + selector: 'cta-button-positive', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is not available -// See SES-4930 export class CTAFeature extends LocatorsInterface { private index: number; @@ -177,13 +171,15 @@ export class CTAFeature extends LocatorsInterface { selector: `new UiSelector().resourceId("cta-feature-${this.index}").childSelector(new UiSelector().className("android.widget.TextView"))`, } as const; case 'ios': - throw new Error('CTAFeature locator is not available on iOS'); + // iOS feature indexing starts at 1, Android at 0 + return { + strategy: 'accessibility id', + selector: `cta-feature-${this.index + 1}`, + } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA -// See SES-4930 export class CTAHeading extends LocatorsInterface { public build() { switch (this.platform) { @@ -194,8 +190,8 @@ export class CTAHeading extends LocatorsInterface { } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Session Needs')]`, + strategy: 'accessibility id', + selector: 'cta-heading', } as const; } } diff --git a/run/test/locators/index.ts b/run/test/locators/index.ts index bde5766884..603d02f336 100644 --- a/run/test/locators/index.ts +++ b/run/test/locators/index.ts @@ -51,8 +51,8 @@ export class BlockedContactsSettings extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'qa-blocked-contacts-settings-item', + strategy: 'id', + selector: 'preferences-option-blocked-contacts', }; case 'ios': return { @@ -311,10 +311,25 @@ export class FirstGif extends LocatorsInterface { } } -export class ImageName extends LocatorsInterface { +export class GIFName extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { // Dates can wildly differ between emulators but it will begin with "Photo taken on" on Android + case 'android': + return { + strategy: 'xpath', + selector: `//*[starts-with(@content-desc, "GIF taken on")]`, + }; + case 'ios': + throw new Error(`No such element on iOS`); + } + } +} + +export class ImageName extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + // Dates can wildly differ between emulators but it will begin with "GIF taken on" on Android case 'android': return { strategy: 'xpath', @@ -456,8 +471,7 @@ export class ReadReceiptsButton extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'android:id/summary', - text: 'Show read receipts for all messages you send and receive.', + selector: 'preferences-option-read-receipt', } as const; case 'ios': return { @@ -536,6 +550,14 @@ export function describeLocator(locator: StrategyExtractionObj & { text?: string ? `${selector.substring(0, halfLength)}…${selector.substring(selector.length - halfLength)}` : selector; + // Trim text if too long, show beginning and end + const maxTextLength = 100; + const trimmedText = text + ? text.length > maxTextLength + ? `${text.substring(0, maxTextLength / 2)}…${text.substring(text.length - maxTextLength / 2)}` + : text + : undefined; + const base = `${strategy} "${trimmedSelector}"`; - return text ? `${base} and text "${text}"` : base; + return trimmedText ? `${base} and text "${trimmedText}"` : base; } diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index 0352e10e5b..a1f6250771 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -153,6 +153,7 @@ export class HideRecoveryPasswordButton extends LocatorsInterface { } } } + export class NotificationsMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -170,7 +171,6 @@ export class NotificationsMenuItem extends LocatorsInterface { } } } - export class PathMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -208,8 +208,9 @@ export class RecoveryPasswordMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Recovery password menu item', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Recovery password menu item"))', } as const; case 'ios': return { @@ -270,6 +271,7 @@ export class SaveNameChangeButton extends LocatorsInterface { } } } + export class SaveProfilePictureButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -303,6 +305,22 @@ export class SelectAppIcon extends LocatorsInterface { } } } +export class SettingsModalsEnableButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'preferences-dialog-option-enable', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Continue', + } as const; + } + } +} export class UserAvatar extends LocatorsInterface { public build() { diff --git a/run/test/media/animated_profile_picture.gif b/run/test/media/animated_profile_picture.gif new file mode 100644 index 0000000000..e609338d16 --- /dev/null +++ b/run/test/media/animated_profile_picture.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cbfc07234028c655e51d9a3d218061c2363b314b4ce59101ce0bfac6e085d77 +size 33041 diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 94e289679f..0495dfd412 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -1,6 +1,6 @@ import test, { type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; @@ -68,15 +68,15 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); @@ -138,15 +138,15 @@ async function banAndDelete(platform: SupportedPlatformsType, testInfo: TestInfo }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index 06e612a7f1..d17581a4be 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -1,6 +1,6 @@ import { test, type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { EmojiReactsPill, FirstEmojiReact, MessageBody } from '../locators/conversation'; @@ -36,11 +36,16 @@ async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, test }); }); await Promise.all( - [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) + [alice1, bob1].map(device => + joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name) + ) + ); + await test.step( + TestSteps.SEND.MESSAGE(alice.userName, communities.testCommunity.name), + async () => { + await alice1.sendMessage(message); + } ); - await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { - await alice1.sendMessage(message); - }); await test.step(TestSteps.SEND.EMOJI_REACT, async () => { await bob1.scrollToBottom(); await bob1.longPressMessage(new MessageBody(bob1, message)); diff --git a/run/test/specs/community_requests_off.spec.ts b/run/test/specs/community_requests_off.spec.ts index b1e04cbf24..9a72f2b0d9 100644 --- a/run/test/specs/community_requests_off.spec.ts +++ b/run/test/specs/community_requests_off.spec.ts @@ -1,7 +1,7 @@ import { test, type TestInfo } from '@playwright/test'; import { USERNAME } from '@session-foundation/qa-seeder'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { CommunityMessageAuthor, UPMMessageButton } from '../locators/conversation'; @@ -33,13 +33,16 @@ async function blindedMessageRequests(platform: SupportedPlatformsType, testInfo await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( [device1, device2].map(async device => { - await joinCommunity(device, testCommunityLink, testCommunityName); + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); }) ); }); - await test.step(TestSteps.SEND.MESSAGE(USERNAME.BOB, testCommunityName), async () => { - await device2.sendMessage(message); - }); + await test.step( + TestSteps.SEND.MESSAGE(USERNAME.BOB, communities.testCommunity.name), + async () => { + await device2.sendMessage(message); + } + ); await device1.clickOnElementAll(new CommunityMessageAuthor(device1, message)); await test.step(`Verify the 'Message' button in the User Profile Modal is disabled`, async () => { // brief sleep to let the UI settle diff --git a/run/test/specs/community_requests_on.spec.ts b/run/test/specs/community_requests_on.spec.ts index 7862cd7d24..caefd37a86 100644 --- a/run/test/specs/community_requests_on.spec.ts +++ b/run/test/specs/community_requests_on.spec.ts @@ -1,7 +1,7 @@ import { test, type TestInfo } from '@playwright/test'; import { USERNAME } from '@session-foundation/qa-seeder'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { CloseSettings } from '../locators'; @@ -55,17 +55,20 @@ async function blindedMessageRequests(platform: SupportedPlatformsType, testInfo await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( [device1, device2].map(async device => { - await joinCommunity(device, testCommunityLink, testCommunityName); + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); }) ); }); - await test.step(TestSteps.SEND.MESSAGE(bob.userName, testCommunityName), async () => { - // brief sleep to let the UI settle - await sleepFor(1000); - await device2.sendMessage(message); - await device2.navigateBack(); - }); + await test.step( + TestSteps.SEND.MESSAGE(bob.userName, communities.testCommunity.name), + async () => { + // brief sleep to let the UI settle + await sleepFor(1000); + await device2.sendMessage(message); + await device2.navigateBack(); + } + ); await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { await device1.clickOnElementAll(new CommunityMessageAuthor(device1, message)); await sleepFor(500); // brief sleep to let the UI settle diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index 380a4d21f0..6cc65028b6 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -1,6 +1,6 @@ import { test, type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { MessageBody } from '../locators/conversation'; @@ -31,7 +31,9 @@ async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: Te const testImageMessage = `Image message + ${new Date().getTime()} - ${platform}`; await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( - [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) + [alice1, bob1].map(device => + joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name) + ) ); }); await test.step(TestSteps.SEND.IMAGE, async () => { @@ -40,7 +42,7 @@ async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: Te await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await sleepFor(2000); // Give bob some time to receive the image await bob1.scrollToBottom(); - await bob1.onAndroid().trustAttachments(testCommunityName); + await bob1.onAndroid().trustAttachments(communities.testCommunity.name); await bob1.onAndroid().scrollToBottom(); // Trusting attachments scrolls the viewport up a bit so gotta scroll to bottom again await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testImageMessage)); }); diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index cc24909ddf..dd9bc8b4ff 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -1,6 +1,6 @@ import { test, type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationItem } from '../locators/home'; @@ -31,16 +31,21 @@ async function joinCommunityTest(platform: SupportedPlatformsType, testInfo: Tes }); const testMessage = `Test message + ${new Date().getTime()}`; await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); await sleepFor(5000); }); - await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { - await alice1.scrollToBottom(); - await alice1.sendMessage(testMessage); - }); + await test.step( + TestSteps.SEND.MESSAGE(alice.userName, communities.testCommunity.name), + async () => { + await alice1.scrollToBottom(); + await alice1.sendMessage(testMessage); + } + ); await test.step(TestSteps.VERIFY.MESSAGE_SYNCED, async () => { // Has community synced to device 2? - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); + await alice2.waitForTextElementToBePresent( + new ConversationItem(alice2, communities.testCommunity.name) + ); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, alice2); diff --git a/run/test/specs/cta_donate_review.spec.ts b/run/test/specs/cta_donate_review.spec.ts index 6000863d39..267cfa95b0 100644 --- a/run/test/specs/cta_donate_review.spec.ts +++ b/run/test/specs/cta_donate_review.spec.ts @@ -10,7 +10,7 @@ import { ReviewPromptItsGreatButton } from '../locators/home'; import { PathMenuItem, UserSettings } from '../locators/settings'; import { newUser } from '../utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; -import { forceStopAndRestart as forceStopAndRestartApp } from '../utils/utilities'; +import { forceStopAndRestart } from '../utils/utilities'; import { verifyPageScreenshot } from '../utils/verify_screenshots'; bothPlatformsIt({ @@ -41,14 +41,10 @@ async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestI await test.step('Dismiss review prompt and restart the app', async () => { await device.clickOnElementAll(new ReviewPromptItsGreatButton(device)); await device.clickOnElementAll(new CloseSettings(device)); - await forceStopAndRestartApp(device); + await forceStopAndRestart(device); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Donate CTA'), async () => { - await device.checkCTAStrings( - tStripped('donateSessionHelp'), - tStripped('donateSessionDescription'), - [tStripped('donate'), tStripped('maybeLater')] - ); + await device.checkCTA('donate'); }); // There *is* supposed to be a blur on Android but there is a bug on API 34 emulators preventing it from showing await test.step(TestSteps.VERIFY.SCREENSHOT('Donate CTA'), async () => { diff --git a/run/test/specs/cta_donate_time.spec.ts b/run/test/specs/cta_donate_time.spec.ts index 06844e0f0e..c7e1f46c29 100644 --- a/run/test/specs/cta_donate_time.spec.ts +++ b/run/test/specs/cta_donate_time.spec.ts @@ -1,18 +1,12 @@ import test, { TestInfo } from '@playwright/test'; -import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { iosIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { CTAButtonPositive } from '../locators/global'; import { PlusButton } from '../locators/home'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; -import { - closeApp, - IOSTestContext, - openAppOnPlatformSingleDevice, - SupportedPlatformsType, -} from '../utils/open_app'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; import { setIOSFirstInstallDate } from '../utils/time_travel'; // iOS uses app-level time override (customFirstInstallDateTime capability). @@ -42,11 +36,7 @@ async function donateCTAShowsSevenDaysAgo(platform: SupportedPlatformsType, test return { device }; }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Donate CTA'), async () => { - await device.checkCTAStrings( - tStripped('donateSessionHelp'), - tStripped('donateSessionDescription'), - [tStripped('donate'), tStripped('maybeLater')] - ); + await device.checkCTA('donate'); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); @@ -77,7 +67,7 @@ async function donateCTADoesntShowSixDaysAgo(platform: SupportedPlatformsType, t await test.step('Verify Donate CTA does not show', async () => { await Promise.all([ device.waitForTextElementToBePresent(new PlusButton(device)), - device.verifyElementNotPresent(new CTAButtonPositive(device)), + device.verifyNoCTAShows(), ]); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index 1b9773eba3..d80ac55070 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -41,12 +41,7 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te let sentTimestamp: number; // Click conversation options menu (three dots) await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); }); // Check control message is correct on device 2 await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index e1c6b6d702..780122c56a 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -35,12 +35,7 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer // Select disappearing messages option - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); // Get control message based on key from json file await checkDisappearingControlMessage( platform, diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 0ad50a7e1e..dc16293b06 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -41,7 +41,7 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['Group', `Disappear after send option`, time]); + await setDisappearingMessage(alice1, ['Group', `Disappear after send option`, time]); }); await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { // Get correct control message for You setting disappearing messages diff --git a/run/test/specs/disappear_after_send_note_to_self.spec.ts b/run/test/specs/disappear_after_send_note_to_self.spec.ts index f5d77399d3..42199065d8 100644 --- a/run/test/specs/disappear_after_send_note_to_self.spec.ts +++ b/run/test/specs/disappear_after_send_note_to_self.spec.ts @@ -46,11 +46,7 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, te }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { // Enable disappearing messages - await setDisappearingMessage(platform, device, [ - 'Note to Self', - 'Disappear after send option', - time, - ]); + await setDisappearingMessage(device, ['Note to Self', 'Disappear after send option', time]); await sleepFor(1000); await device.waitForControlMessageToBePresent( `You set messages to disappear ${time} after they have been ${controlMode}.` diff --git a/run/test/specs/disappear_after_send_off_1o1.spec.ts b/run/test/specs/disappear_after_send_off_1o1.spec.ts index fcef4670d0..998d9688c6 100644 --- a/run/test/specs/disappear_after_send_off_1o1.spec.ts +++ b/run/test/specs/disappear_after_send_off_1o1.spec.ts @@ -12,7 +12,6 @@ import { SetDisappearMessagesButton, } from '../locators/disappearing_messages'; import { open_Alice2_Bob1_friends } from '../state_builder'; -import { sleepFor } from '../utils'; import { checkDisappearingControlMessage } from '../utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; import { setDisappearingMessage } from '../utils/set_disappearing_messages'; @@ -40,12 +39,7 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType, testIn const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; // Select disappearing messages option - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); // Get control message based on key from json file await checkDisappearingControlMessage( platform, @@ -78,7 +72,6 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType, testIn ]); // Follow setting on device 2 await bob1.clickOnElementAll(new FollowSettingsButton(bob1)); - await sleepFor(500); await bob1.checkModalStrings( tStripped('disappearingMessagesFollowSetting'), tStripped('disappearingMessagesFollowSettingOff') diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index e7c16ac421..09ba62b0a4 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -5,7 +5,8 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { CloseSettings } from '../locators'; -import { CallButton, NotificationsModalButton, NotificationSwitch } from '../locators/conversation'; +import { CallButton, NotificationSwitch } from '../locators/conversation'; +import { SettingsModalsEnableButton } from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; import { sleepFor } from '../utils'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; @@ -43,7 +44,7 @@ async function disappearingCallMessage1o1Ios(platform: SupportedPlatformsType, t focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); await alice1.clickOnElementAll(new CallButton(alice1)); // Alice turns on all calls perms necessary (without checking every modal string) await alice1.clickOnByAccessibilityID('Settings'); @@ -126,18 +127,18 @@ async function disappearingCallMessage1o1Android( focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); await alice1.clickOnElementAll(new CallButton(alice1)); // Alice turns on all calls perms necessary (without checking every modal string) await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Settings', }); - await alice1.clickOnByAccessibilityID('Enable'); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); - await alice1.clickOnElementAll(new NotificationsModalButton(alice1)); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); await alice1.clickOnElementAll(new NotificationSwitch(alice1)); // Return to conversation await alice1.navigateBack(false); diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 620c027bfd..d529f85464 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -1,6 +1,6 @@ import type { TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { InviteContactsMenuItem } from '../locators'; @@ -47,10 +47,10 @@ async function disappearingCommunityInviteMessage( focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); // await alice1.navigateBack(); await alice1.navigateBack(); - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); await alice1.clickOnElementAll(new ConversationSettings(alice1)); await sleepFor(1000); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index 812e47773e..7a0a3853ce 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -33,7 +33,7 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); const sentTimestamp = await alice1.sendGIF(); await bob1.trustAttachments(USERNAME.ALICE); await Promise.all( diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index afa7d4313e..cdd35573e5 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -33,7 +33,7 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); const sentTimestamp = await alice1.sendImage(testMessage); await bob1.trustAttachments(alice.userName); if (platform === 'ios') { diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index adf01dc066..4ac26ff1b3 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -51,7 +51,7 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { await alice1.inputText(testLink, new MessageInput(alice1)); @@ -105,7 +105,7 @@ async function disappearingLinkMessage1o1Android( }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time]); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { await alice1.inputText(testLink, new MessageInput(alice1)); diff --git a/run/test/specs/disappearing_messages_defaults.spec.ts b/run/test/specs/disappearing_messages_defaults.spec.ts new file mode 100644 index 0000000000..a356de22e9 --- /dev/null +++ b/run/test/specs/disappearing_messages_defaults.spec.ts @@ -0,0 +1,126 @@ +import type { TestInfo } from '@playwright/test'; + +import { bothPlatformsIt } from '../../types/sessionIt'; +import { DISAPPEARING_TIMES, GROUPNAME, USERNAME } from '../../types/testing'; +import { ConversationSettings } from '../locators/conversation'; +import { + DisappearingMessagesMenuOption, + DisappearingMessagesTimerType, +} from '../locators/disappearing_messages'; +import { PlusButton } from '../locators/home'; +import { EnterAccountID, NewMessageOption, NextButton } from '../locators/start_conversation'; +import { + open_Alice1_Bob1_Charlie1_friends_group, + open_Alice1_Bob1_friends, +} from '../state_builder'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; + +bothPlatformsIt({ + title: 'Disappearing messages defaults 1:1', + risk: 'medium', + testCb: disappearingMessagesDefaults1o1, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: 'Verifies the default selected timer for each DM mode in a 1:1 conversation', +}); + +bothPlatformsIt({ + title: 'Disappearing messages defaults group', + risk: 'medium', + testCb: disappearingMessagesDefaultsGroup, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: 'Verifies the default selected timer in a group conversation', +}); + +bothPlatformsIt({ + title: 'Disappearing messages defaults note to self', + risk: 'medium', + testCb: disappearingMessagesDefaultsNoteToSelf, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: 'Verifies the default selected timer in Note to Self', +}); + +async function disappearingMessagesDefaults1o1( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const { + devices: { alice1, bob1 }, + } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); + + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new DisappearingMessagesMenuOption(alice1)); + + // Disappear after read: default should be 12 hours + await alice1.clickOnElementAll( + new DisappearingMessagesTimerType(alice1, 'Disappear after read option') + ); + await alice1.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.TWELVE_HOURS); + + // Disappear after send: default should be 1 day + await alice1.clickOnElementAll( + new DisappearingMessagesTimerType(alice1, 'Disappear after send option') + ); + await alice1.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); + + await closeApp(alice1, bob1); +} + +async function disappearingMessagesDefaultsGroup( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const testGroupName: GROUPNAME = 'Testing disappearing messages'; + const { + devices: { alice1, bob1, charlie1 }, + } = await open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new DisappearingMessagesMenuOption(alice1)); + + // Group defaults: disappear after send should be OFF + await alice1.onIOS().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_IOS); + await alice1.onAndroid().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_ANDROID); + + await closeApp(alice1, bob1, charlie1); +} + +async function disappearingMessagesDefaultsNoteToSelf( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const alice = await newUser(device, USERNAME.ALICE); + + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new NewMessageOption(device)); + await device.inputText(alice.accountID, new EnterAccountID(device)); + await device.scrollDown(); + await device.clickOnElementAll(new NextButton(device)); + + await device.clickOnElementAll(new ConversationSettings(device)); + await device.clickOnElementAll(new DisappearingMessagesMenuOption(device)); + + // Note to Self defaults: disappear after send should be OFF + await device.onIOS().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_IOS); + await device.onAndroid().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_ANDROID); + + await closeApp(device); +} diff --git a/run/test/specs/disappearing_messages_follow_settings.spec.ts b/run/test/specs/disappearing_messages_follow_settings.spec.ts new file mode 100644 index 0000000000..49955f0134 --- /dev/null +++ b/run/test/specs/disappearing_messages_follow_settings.spec.ts @@ -0,0 +1,60 @@ +import type { TestInfo } from '@playwright/test'; + +import { tStripped } from '../../localizer/lib'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { DISAPPEARING_TIMES } from '../../types/testing'; +import { + DisappearingMessagesSubtitle, + FollowSettingsButton, + SetModalButton, +} from '../locators/disappearing_messages'; +import { open_Alice1_Bob1_friends } from '../state_builder'; +import { closeApp, SupportedPlatformsType } from '../utils/open_app'; +import { setDisappearingMessage } from '../utils/set_disappearing_messages'; + +bothPlatformsIt({ + title: 'Disappearing messages follow setting 1:1', + risk: 'medium', + testCb: disappearingMessagesFollowSetting1o1, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: + 'Verifies that Bob sees the Follow Setting banner when Alice sets disappearing messages in a 1:1 conversation, and that following applies the setting to both sides', +}); + +const time = DISAPPEARING_TIMES.THIRTY_SECONDS; +const timerType = 'Disappear after send option'; + +async function disappearingMessagesFollowSetting1o1( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const { + devices: { alice1, bob1 }, + } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); + + await setDisappearingMessage(alice1, ['1:1', timerType, time]); + + // Bob should see the follow settings banner after Alice sets DM + await bob1.clickOnElementAll(new FollowSettingsButton(bob1)); + await bob1.checkModalStrings( + tStripped('disappearingMessagesFollowSetting'), + tStripped('disappearingMessagesFollowSettingOn', { + time, + disappearing_messages_type: 'sent', + }) + ); + await bob1.clickOnElementAll(new SetModalButton(bob1)); + + // Both should now show the DM subtitle + await Promise.all( + [alice1, bob1].map(device => + device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)) + ) + ); + + await closeApp(alice1, bob1); +} diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index 9c81c6d418..fa36394514 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -35,7 +35,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); let sentTimestamp: number; if (platform === 'ios') { sentTimestamp = await alice1.onIOS().sendVideoiOS(testMessage); diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index ba6718a829..26c218904c 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -32,7 +32,7 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); const sentTimestamp = await alice1.sendVoiceMessage(); await bob1.trustAttachments(alice.userName); await Promise.all( diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index 9c9c70eef0..cec83e9dfd 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -35,7 +35,7 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes focusGroupConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); // Click on attachments button const sentTimestamp = await alice1.sendGIF(); await Promise.all( diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 47ffbddf62..762bced731 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -34,7 +34,7 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); const sentTimestamp = await alice1.sendImage(testMessage); if (platform === 'ios') { await Promise.all( diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index 226834583d..ec9ef96f91 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -47,7 +47,7 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { await alice1.inputText(testLink, new MessageInput(alice1)); diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index 20d3d048c1..1121e05a57 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -36,7 +36,7 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t focusGroupConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); let sentTimestamp: number; if (platform === 'ios') { sentTimestamp = await alice1.sendVideoiOS(testMessage); diff --git a/run/test/specs/group_disappearing_messages_voice.spec.ts b/run/test/specs/group_disappearing_messages_voice.spec.ts index 25862d0a7c..b933dcc83c 100644 --- a/run/test/specs/group_disappearing_messages_voice.spec.ts +++ b/run/test/specs/group_disappearing_messages_voice.spec.ts @@ -31,7 +31,7 @@ async function disappearingVoiceMessageGroup(platform: SupportedPlatformsType, t focusGroupConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); const sentTimestamp = await alice1.sendVoiceMessage(); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index 028cd5ffdc..3cd346c330 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -40,7 +40,9 @@ async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: device.waitForTextElementToBePresent(new VoiceMessage(device)) ) ); - await bob1.longPressMessage(new VoiceMessage(bob1)); + // The voice message long tap must be offset so that it doesn't tap the scrubber + // As this starts playback and does not open the long press menu + await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 50 } }); await bob1.clickOnByAccessibilityID('Reply to message'); await sleepFor(500); // Let the UI settle before finding message input and typing await bob1.sendMessage(replyMessage); diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index 2b70d03f4d..283da19a0d 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -52,7 +52,7 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T const userD = await test.step(TestSteps.SETUP.NEW_USER, async () => { return newUser(unknown1, USERNAME.DRACULA); }); - const aliceTruncatedPubkey = truncatePubkey(alice.sessionId, platform); + const aliceTruncatedPubkey = truncatePubkey(alice.accountID, platform); const historicMsg = `Hello from ${alice.userName}`; const userDTruncatedPubkey = truncatePubkey(userD.accountID, platform); const userDMsg = `Hello from ${userD.userName}`; diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index 0420b68853..5ad2cf7971 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -134,7 +134,7 @@ async function promoteSoloToAdmin(platform: SupportedPlatformsType, testInfo: Te await alice1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await setDisappearingMessage(bob1, ['Group', timerType, time]); await Promise.all( [alice1, charlie1].map(device => device.waitForControlMessageToBePresent( @@ -224,7 +224,7 @@ async function promoteSoloLinked(platform: SupportedPlatformsType, testInfo: Tes await device1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, device2, ['Group', timerType, time]); + await setDisappearingMessage(device2, ['Group', timerType, time]); await Promise.all( [device1, device3].map(device => device.waitForControlMessageToBePresent( @@ -331,7 +331,7 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T await alice1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await setDisappearingMessage(bob1, ['Group', timerType, time]); await Promise.all( [alice1, charlie1].map(device => device.waitForControlMessageToBePresent( @@ -351,7 +351,7 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T await test.step(`Verify ${charlie.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages const charlieTime = DISAPPEARING_TIMES.TWELVE_HOURS; - await setDisappearingMessage(platform, charlie1, ['Group', timerType, charlieTime]); + await setDisappearingMessage(charlie1, ['Group', timerType, charlieTime]); await Promise.all( [alice1, bob1].map(device => device.waitForControlMessageToBePresent( diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index 2cba89667a..c09eb3a300 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -1,10 +1,6 @@ import test, { type TestInfo } from '@playwright/test'; -import { - testCommunityLink, - testCommunityName, - unresolvedTestcommunityName, -} from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; @@ -78,15 +74,15 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); @@ -107,7 +103,7 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { await restoreAccount(bob2, bob, 'bob2'); - await bob2.clickOnElementAll(new ConversationItem(alice1, unresolvedTestcommunityName)); // Since we're banned we don't get the "real" name + await bob2.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.roomName)); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); await bob2.onIOS().waitForTextElementToBePresent({ strategy: 'xpath', @@ -152,15 +148,15 @@ async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: Te }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); @@ -178,7 +174,7 @@ async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: Te }); await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { await restoreAccount(bob2, bob, 'bob2'); - await bob2.clickOnElementAll(new ConversationItem(alice1, unresolvedTestcommunityName)); // Since we're banned we don't get the "real" name + await bob2.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.roomName)); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); }); await test.step('Verify Bob cannot send messages in community on either device', async () => { diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index 7d622ae4d3..20de78138e 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -1,6 +1,6 @@ import type { TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { bothPlatformsIt } from '../../types/sessionIt'; import { InviteContactsMenuItem, JoinCommunityModalButton } from '../locators'; @@ -35,7 +35,7 @@ async function sendCommunityInvitation(platform: SupportedPlatformsType, testInf // Join community on device 1 // Click on plus button await alice1.navigateBack(); - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); await alice1.clickOnElementAll(new ConversationSettings(alice1)); await sleepFor(500); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); @@ -45,10 +45,12 @@ async function sendCommunityInvitation(platform: SupportedPlatformsType, testInf await bob1.clickOnElementAll(new CommunityInvitation(bob1)); await bob1.checkModalStrings( tStripped('communityJoin'), - tStripped('communityJoinDescription', { community_name: testCommunityName }) + tStripped('communityJoinDescription', { community_name: communities.testCommunity.name }) ); await bob1.clickOnElementAll(new JoinCommunityModalButton(bob1)); await bob1.navigateBack(); - await bob1.waitForTextElementToBePresent(new ConversationItem(bob1, testCommunityName)); + await bob1.waitForTextElementToBePresent( + new ConversationItem(bob1, communities.testCommunity.name) + ); await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 95e4fa386b..006cdc7325 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -14,46 +14,95 @@ import { import { CTAButtonNegative } from '../locators/global'; import { PlusButton } from '../locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from '../locators/start_conversation'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; +import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { forceStopAndRestart } from '../utils/utilities'; -const maxChars = 2000; -const countdownThreshold = 1800; +const STANDARD_MAX_CHARS = 2000; +const PRO_MAX_CHARS = 10000; +const COUNTDOWN_START_THRESHOLD = 200; const messageLengthTestCases = [ { + pro: false, length: 1799, - char: 'a', shouldSend: true, description: 'no countdown shows, message sends', }, - { length: 1800, char: 'b', shouldSend: true, description: 'countdown shows 200, message sends' }, - { length: 2000, char: 'c', shouldSend: true, description: 'countdown shows 0, message sends' }, { + pro: false, + length: 1800, + shouldSend: true, + description: 'countdown shows 200, message sends', + }, + { + pro: false, + length: 2000, + shouldSend: true, + description: 'countdown shows 0, message sends', + }, + { + pro: false, length: 2001, - char: 'd', + shouldSend: false, + description: 'countdown shows -1, cannot send message', + }, + { + pro: true, + length: 9799, + shouldSend: true, + description: 'no countdown shows, message sends', + }, + { + pro: true, + length: 9800, + shouldSend: true, + description: 'countdown shows 200, message sends', + }, + { + pro: true, + length: 10000, + shouldSend: true, + description: 'countdown shows 0, message sends', + }, + { + pro: true, + length: 10001, shouldSend: false, description: 'countdown shows -1, cannot send message', }, ]; for (const testCase of messageLengthTestCases) { + const proSuffix = testCase.pro ? `Pro` : `non Pro`; bothPlatformsIt({ - title: `Message length limit (${testCase.length} chars)`, + title: `Message length limit (${testCase.length} chars ${proSuffix})`, risk: 'high', countOfDevicesNeeded: 1, allureSuites: { parent: 'Sending Messages', suite: 'Rules', }, - allureDescription: `Verifies message length behavior at ${testCase.length} characters - ${testCase.description}`, + allureDescription: `Verifies message length behavior at ${testCase.length} characters - ${testCase.description} (${proSuffix})`, testCb: async (platform: SupportedPlatformsType, testInfo: TestInfo) => { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); + if (testCase.pro) { + await makeAccountPro({ user: alice, platform }); + // Restart to notify app of Pro status change + await forceStopAndRestart(device); + await device.dismissCTA(); + } + // Send message to self to bring up Note to Self conversation await test.step(TestSteps.OPEN.NTS, async () => { await device.clickOnElementAll(new PlusButton(device)); @@ -64,12 +113,15 @@ for (const testCase of messageLengthTestCases) { }); await test.step(`Type ${testCase.length} chars, check countdown`, async () => { + const expectedMax = testCase.pro ? PRO_MAX_CHARS : STANDARD_MAX_CHARS; const expectedCount = - testCase.length < countdownThreshold ? null : (maxChars - testCase.length).toString(); + testCase.length < expectedMax - COUNTDOWN_START_THRESHOLD + ? null + : (expectedMax - testCase.length).toString(); // Construct the string of desired length - const message = testCase.char.repeat(testCase.length); - await device.inputText(message, new MessageInput(device)); + const message = 'x'.repeat(testCase.length); + await device.inputText(message, new MessageInput(device), true); // Does the countdown appear? if (expectedCount) { @@ -85,30 +137,19 @@ for (const testCase of messageLengthTestCases) { // Is the message short enough to send? if (testCase.shouldSend) { await device.waitForTextElementToBePresent(new MessageBody(device, message)); - } else if (platform === 'ios') { - // iOS: Modal appears, verify and dismiss + } else if (!testCase.pro) { + // For Non Pro, a CTA appears + await device.checkCTA('longerMessages'); + await device.clickOnElementAll(new CTAButtonNegative(device)); + await device.verifyElementNotPresent(new MessageBody(device, message)); + } else if (testCase.pro) { + // For Pro, a normal message length dialog appears await device.checkModalStrings( tStripped('modalMessageTooLongTitle'), - tStripped('modalMessageTooLongDescription', { limit: maxChars.toString() }) + tStripped('modalMessageTooLongDescription', { limit: expectedMax.toString() }) ); await device.clickOnElementAll(new MessageLengthOkayButton(device)); await device.verifyElementNotPresent(new MessageBody(device, message)); - } else { - // Android: CTA appears, verify and dismiss - // Post-Pro is active on debug/qa builds by default - // This will be the default for both platforms once Pro is live - await device.checkCTAStrings( - tStripped('upgradeTo'), - tStripped('proCallToActionLongerMessages'), - [tStripped('theContinue'), tStripped('cancel')], - [ - tStripped('proFeatureListLongerMessages'), - tStripped('proFeatureListPinnedConversations'), - tStripped('proFeatureListLoadsMore'), - ] - ); - await device.clickOnElementAll(new CTAButtonNegative(device)); - await device.verifyElementNotPresent(new MessageBody(device, message)); } }); diff --git a/run/test/specs/message_voice.spec.ts b/run/test/specs/message_voice.spec.ts index e5802e79de..68164a123b 100644 --- a/run/test/specs/message_voice.spec.ts +++ b/run/test/specs/message_voice.spec.ts @@ -29,7 +29,9 @@ async function sendVoiceMessage(platform: SupportedPlatformsType, testInfo: Test await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); await bob1.trustAttachments(alice.userName); await sleepFor(500); - await bob1.longPressMessage(new VoiceMessage(bob1)); + // The voice message long tap must be offset so that it doesn't tap the scrubber + // As this starts playback and does not open the long press menu + await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 50 } }); await bob1.clickOnByAccessibilityID('Reply to message'); await sleepFor(500); // Let the UI settle before finding message input and typing await bob1.sendMessage(replyMessage); diff --git a/run/test/specs/recovery_banner.spec.ts b/run/test/specs/recovery_banner.spec.ts new file mode 100644 index 0000000000..c71967f64c --- /dev/null +++ b/run/test/specs/recovery_banner.spec.ts @@ -0,0 +1,128 @@ +import { test, type TestInfo } from '@playwright/test'; +import { USERNAME } from '@session-foundation/qa-seeder'; + +import { communities } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; +import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { androidIt } from '../../types/sessionIt'; +import { ConversationItem, PlusButton } from '../locators/home'; +import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; +import { joinCommunity } from '../utils/community'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; + +androidIt({ + title: 'Recovery password banner only shows after 3 conversations', + risk: 'medium', + testCb: bannerShowsThreeConvos, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Recovery Password', + }, + allureDescription: + 'Verifies that the recovery password banner only shows after the user has at least three conversations.', +}); + +androidIt({ + title: 'Recovery password banner disappears after being opened', + risk: 'medium', + testCb: bannerDisappearsAfterOpened, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Recovery Password', + }, + allureDescription: 'Verifies that the recovery password banner disappears after first opened.', +}); + +androidIt({ + title: 'Recovery password banner persists with less than 3 conversations', + risk: 'medium', + testCb: bannerPersists, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Recovery Password', + }, + allureDescription: + 'Verifies that the recovery password banner does not disappear if the conversation count drops below 3', +}); + +async function bannerShouldNotshow(device: DeviceWrapper) { + await device.waitForTextElementToBePresent(new PlusButton(device)); + await device.verifyElementNotPresent(new RevealRecoveryPhraseButton(device)); + device.log('On home screen, banner did not appear'); +} + +async function bannerShouldShow(device: DeviceWrapper) { + await device.waitForTextElementToBePresent(new PlusButton(device)); + await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); + device.log('On home screen, banner appeared'); +} + +async function bannerShowsThreeConvos(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Create three conversations, verify banner only appears after the third', async () => { + for (const community of Object.values(communities).slice(0, 3)) { + await bannerShouldNotshow(device); + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } + await bannerShouldShow(device); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function bannerDisappearsAfterOpened(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Create three conversations, verify banner does not reappear after being opened', async () => { + for (const community of Object.values(communities).slice(0, 3)) { + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } + await bannerShouldShow(device); + await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + await device.waitForTextElementToBePresent(new RecoveryPhraseContainer(device)); + await device.navigateBack(); + await bannerShouldNotshow(device); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function bannerPersists(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Create three conversations, verify banner persists after a conversation is deleted', async () => { + for (const community of Object.values(communities).slice(0, 3)) { + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } + await bannerShouldShow(device); + await device.longPressConversation(communities.testCommunity.name); + await device.clickOnElementAll({ strategy: 'accessibility id', selector: 'Leave' }); // Long press options + await device.clickOnElementAll({ strategy: 'accessibility id', selector: 'Leave' }); // Modal confirm + await device.verifyElementNotPresent( + new ConversationItem(device, communities.testCommunity.name) + ); + await bannerShouldShow(device); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} diff --git a/run/test/specs/review_positive.spec.ts b/run/test/specs/review_positive.spec.ts index 86a8ff2687..8de5d64916 100644 --- a/run/test/specs/review_positive.spec.ts +++ b/run/test/specs/review_positive.spec.ts @@ -28,11 +28,6 @@ bothPlatformsIt({ async function reviewPromptPositive(platform: SupportedPlatformsType, testInfo: TestInfo) { const storevariant = platform === 'android' ? 'Google Play Store' : 'App Store'; - // Platform specific string for the Rate Session modal - const rateModalDescriptionString = - platform === 'android' - ? tStripped('rateSessionModalDescription', { storevariant }) - : tStripped('rateSessionModalDescriptionUpdated', { storevariant }); const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); await newUser(device, USERNAME.ALICE, { saveUserData: false }); @@ -52,9 +47,12 @@ async function reviewPromptPositive(platform: SupportedPlatformsType, testInfo: await device.clickOnElementAll(new ReviewPromptItsGreatButton(device)); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Rate Session'), async () => { - await device.checkModalStrings(tStripped('rateSession'), rateModalDescriptionString); + await device.checkModalStrings( + tStripped('rateSession'), + tStripped('rateSessionModalDescriptionUpdated', { storevariant }) + ); await device.waitForTextElementToBePresent(new ReviewPromptRateAppButton(device)); - await device.onAndroid().waitForTextElementToBePresent(new ReviewPromptNotNowButton(device)); // On iOS the modal only has the Rate button + await device.verifyElementNotPresent(new ReviewPromptNotNowButton(device)); // This modal now only has the Rate button }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); diff --git a/run/test/specs/slow_mode_background.spec.ts b/run/test/specs/slow_mode_background.spec.ts index 62cff9a222..9d1a331f17 100644 --- a/run/test/specs/slow_mode_background.spec.ts +++ b/run/test/specs/slow_mode_background.spec.ts @@ -1,10 +1,11 @@ -import test, { TestInfo } from '@playwright/test'; +import { test, TestInfo } from '@playwright/test'; import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { BackgroundPermsAllowButton } from '../locators/home'; +import { NotificationsMenuItem, UserSettings } from '../locators/settings'; import { newUser } from '../utils/create_account'; import { closeApp, @@ -48,7 +49,18 @@ async function slowModeBackgroundModal(platform: SupportedPlatformsType, testInf text: 'Allow', }); }); - // The test ends here since there is no good way to verify that the specific toggle is ON. + await test.step('Verify Background usage toggle is turned ON', async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new NotificationsMenuItem(device)); + await device.assertAttribute( + { + strategy: 'id', + selector: 'preferences-option-whitelist-toggle', + }, + 'checked', + 'true' + ); + }); } finally { // App must be uninstalled to prevent state pollution (background permission is tied to app install) await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/upm_homescreen.spec.ts b/run/test/specs/upm_homescreen.spec.ts index 1ef5c7ee65..aad3222c87 100644 --- a/run/test/specs/upm_homescreen.spec.ts +++ b/run/test/specs/upm_homescreen.spec.ts @@ -48,7 +48,7 @@ async function upmHomeScreen(platform: SupportedPlatformsType, testInfo: TestInf }); const elText = await alice1.getTextFromElement(el); const normalized = elText.replace(/\s+/g, ''); // account id comes in two lines - const expected = bob.sessionId.trim(); + const expected = bob.accountID.trim(); if (normalized !== expected) { console.log(`Expected: ${expected} Observed: ${normalized}`); diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts new file mode 100644 index 0000000000..1f1e0153b9 --- /dev/null +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -0,0 +1,156 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { tStripped } from '../../localizer/lib'; +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { USERNAME } from '../../types/testing'; +import { CloseSettings } from '../locators'; +import { ConversationSettings, MessageBody } from '../locators/conversation'; +import { ConversationItem } from '../locators/home'; +import { PathMenuItem, UserAvatar, UserSettings } from '../locators/settings'; +import { open_Alice1_Bob1_friends } from '../state_builder'; +import { IOSTestContext } from '../utils/capabilities_ios'; +import { newUser } from '../utils/create_account'; +import { makeAccountPro } from '../utils/mock_pro'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { forceStopAndRestart } from '../utils/utilities'; +import { verifyPageScreenshot } from '../utils/verify_screenshots'; + +bothPlatformsIt({ + title: 'Upload animated profile picture (non Pro)', + risk: 'high', + countOfDevicesNeeded: 1, + testCb: nonProAnimatedDP, + allureSuites: { + parent: 'User Actions', + suite: 'Change Profile Picture', + }, +}); + +bothPlatformsIt({ + title: 'Upload animated profile picture (Pro)', + risk: 'high', + countOfDevicesNeeded: 1, + testCb: proAnimatedDP, + allureSuites: { + parent: 'User Actions', + suite: 'Change Profile Picture', + }, +}); + +bothPlatformsIt({ + title: 'Pro Activated CTA', + risk: 'low', + countOfDevicesNeeded: 1, + testCb: proActivatedCTA, + allureSuites: { + parent: 'Session Pro', + }, +}); + +bothPlatformsIt({ + title: 'Animated Profile Picture shows', + risk: 'high', + countOfDevicesNeeded: 2, + testCb: proAnimatedDPShows, + allureSuites: { + parent: 'Session Pro', + }, +}); + +async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { + await device.uploadProfilePicture(true); + await device.checkCTA('animatedProfilePicture'); + }); + + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} +async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); + await makeAccountPro({ user: alice, platform }); + await forceStopAndRestart(device); + await test.step('Verify Pro Activated CTA', async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new UserAvatar(device)); + await device.clickOnElementAll({ + strategy: 'id', + selector: 'pro-badge-text', + text: tStripped('proAnimatedDisplayPictureModalDescription'), + }); + await verifyPageScreenshot(device, platform, 'cta_pro_activated', testInfo); + await device.checkCTA('alreadyActivated'); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); + await makeAccountPro({ user: alice, platform }); + await forceStopAndRestart(device); + await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { + await device.uploadProfilePicture(true); + }); + await device.waitForTextElementToBePresent(new PathMenuItem(device)); + await device.verifyNoCTAShows(); + await device.verifyElementIsAnimated(new UserAvatar(device)); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function proAnimatedDPShows(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const { devices, prebuilt } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return await open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: false, + testInfo, + iOSContext: iosContext, + }); + }); + const { alice1, bob1 } = devices; + const { alice, bob } = prebuilt; + await makeAccountPro({ user: alice, platform }); + await forceStopAndRestart(alice1); + await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { + await alice1.uploadProfilePicture(true); + }); + await alice1.clickOnElementAll(new CloseSettings(alice1)); + await alice1.clickOnElementAll(new ConversationItem(alice1, bob.userName)); + await alice1.sendMessage('Howdy'); + await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, 'Howdy')); + await bob1.verifyElementIsAnimated(new ConversationSettings(bob1)); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/test/specs/user_actions_share_to_session.spec.ts b/run/test/specs/user_actions_share_to_session.spec.ts index c05a443075..e5d6af1e55 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -121,7 +121,7 @@ async function shareInSession(platform: SupportedPlatformsType, testInfo: TestIn await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await alice1.waitForTextElementToBePresent(new ConversationHeaderName(alice1, 'Note to Self')); await alice1.waitForTextElementToBePresent(new MessageBody(alice1, testMessage)); - await alice1.matchAndTapImage(new MediaMessage(alice1).build(), testImage); + await alice1.waitForTextElementToBePresent(new MediaMessage(alice1)); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 49925f26a5..c71789ccf9 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -4,7 +4,8 @@ import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { CloseSettings } from '../locators'; -import { CallButton, NotificationsModalButton, NotificationSwitch } from '../locators/conversation'; +import { CallButton, NotificationSwitch } from '../locators/conversation'; +import { SettingsModalsEnableButton } from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; import { sleepFor } from '../utils/index'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; @@ -58,7 +59,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo tStripped('callsVoiceAndVideoModalDescription') ); }); - await alice1.clickOnByAccessibilityID('Continue'); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); // Need to allow microphone access await alice1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); await sleepFor(1_000); @@ -103,7 +104,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo tStripped('callsVoiceAndVideoBeta'), tStripped('callsVoiceAndVideoModalDescription') ); - await bob1.clickOnByAccessibilityID('Continue'); + await bob1.clickOnElementAll(new SettingsModalsEnableButton(bob1)); // Need to allow microphone access await bob1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); await sleepFor(1_000); @@ -178,7 +179,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test tStripped('callsVoiceAndVideoBeta'), tStripped('callsVoiceAndVideoModalDescription') ); - await alice1.clickOnByAccessibilityID('Enable'); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); }); await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' @@ -188,7 +189,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test tStripped('sessionNotifications'), tStripped('callsNotificationsRequired') ); - await alice1.clickOnElementAll(new NotificationsModalButton(alice1)); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); await alice1.clickOnElementAll(new NotificationSwitch(alice1)); }); await alice1.navigateBack(false); @@ -231,11 +232,11 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'accessibility id', selector: 'Settings', }); - await bob1.clickOnByAccessibilityID('Enable'); + await bob1.clickOnElementAll(new SettingsModalsEnableButton(bob1)); await bob1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); - await bob1.clickOnElementAll(new NotificationsModalButton(bob1)); + await bob1.clickOnElementAll(new SettingsModalsEnableButton(bob1)); await bob1.clickOnElementAll(new NotificationSwitch(bob1)); await bob1.navigateBack(false); await bob1.navigateBack(false); diff --git a/run/test/state_builder/index.ts b/run/test/state_builder/index.ts index e448a90121..f6a33901b7 100644 --- a/run/test/state_builder/index.ts +++ b/run/test/state_builder/index.ts @@ -14,16 +14,26 @@ import { } from '@session-foundation/qa-seeder'; import type { DeviceWrapper } from '../../types/DeviceWrapper'; +import type { User } from '../../types/testing'; import { ConversationItem } from '../locators/home'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { getNetworkTarget } from '../utils/devnet'; import { openAppMultipleDevices, type SupportedPlatformsType } from '../utils/open_app'; import { restoreAccountNoFallback } from '../utils/restore_account'; -type WithAlice = { alice: StateUser }; -type WithBob = { bob: StateUser }; -type WithCharlie = { charlie: StateUser }; -type WithDracula = { dracula: StateUser }; +function toUser(stateUser: StateUser): User { + return { + userName: stateUser.userName, + accountID: stateUser.sessionId, + recoveryPhrase: stateUser.seedPhrase, + }; +} + +type WithAlice = { alice: User }; +type WithBob = { bob: User }; +type WithCharlie = { charlie: User }; +type WithDracula = { dracula: User }; type WithFocusFriendsConvo = { focusFriendsConvo: boolean }; type WithFocusGroupConvo = { focusGroupConvo: boolean }; @@ -91,14 +101,16 @@ async function openAppsWithState m.seedPhrase); await linkDevices(result.devices, seedPhrases); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); const alice1 = result.devices[0]; const bob1 = result.devices[1]; const formattedDevices = { @@ -156,10 +170,12 @@ export async function open_Alice1_Bob1_Charlie1_friends_group({ groupName, focusGroupConvo, testInfo, + iOSContext, }: WithPlatform & WithFocusGroupConvo & { groupName: string; testInfo: TestInfo; + iOSContext?: IOSTestContext; }) { const stateToBuildKey = '3friendsInGroup'; const appsToOpen = 3; @@ -169,6 +185,7 @@ export async function open_Alice1_Bob1_Charlie1_friends_group({ stateToBuildKey, groupName, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); @@ -177,9 +194,9 @@ export async function open_Alice1_Bob1_Charlie1_friends_group({ const seedPhrases = result.prebuilt.users.map(m => m.seedPhrase); await linkDevices(result.devices, seedPhrases); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; - const charlie = result.prebuilt.users[2]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); + const charlie = toUser(result.prebuilt.users[2]); const alice1 = result.devices[0]; const bob1 = result.devices[1]; @@ -214,10 +231,12 @@ export async function open_Alice2_Bob1_Charlie1_friends_group({ groupName, focusGroupConvo, testInfo, + iOSContext, }: WithPlatform & WithFocusGroupConvo & { groupName: string; testInfo: TestInfo; + iOSContext?: IOSTestContext; }) { const stateToBuildKey = '3friendsInGroup'; const appsToOpen = 4; @@ -227,15 +246,23 @@ export async function open_Alice2_Bob1_Charlie1_friends_group({ stateToBuildKey, groupName, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); result.devices[2].setDeviceIdentity('charlie1'); result.devices[3].setDeviceIdentity('alice2'); - const [alice, bob, charlie] = result.prebuilt.users; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); + const charlie = toUser(result.prebuilt.users[2]); - const seedPhrases = [alice.seedPhrase, bob.seedPhrase, charlie.seedPhrase, alice.seedPhrase]; + const seedPhrases = [ + alice.recoveryPhrase, + bob.recoveryPhrase, + charlie.recoveryPhrase, + alice.recoveryPhrase, + ]; await linkDevices(result.devices, seedPhrases); const [alice1, bob1, charlie1, alice2] = result.devices; @@ -275,10 +302,12 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ groupName, focusGroupConvo = true, testInfo, + iOSContext, }: WithPlatform & WithFocusGroupConvo & { groupName: string; testInfo: TestInfo; + iOSContext?: IOSTestContext; }) { const stateToBuildKey = '3friendsInGroup'; const appsToOpen = 4; @@ -288,6 +317,7 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ stateToBuildKey, groupName, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); @@ -308,9 +338,9 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ charlie1, unknown1: result.devices[3], // not assigned yet }; - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; - const charlie = result.prebuilt.users[2]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); + const charlie = toUser(result.prebuilt.users[2]); const formattedUsers: WithUsers<3> = { alice, bob, @@ -330,7 +360,11 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ }; } -export async function open_Alice2({ platform, testInfo }: WithPlatform & { testInfo: TestInfo }) { +export async function open_Alice2({ + platform, + testInfo, + iOSContext, +}: WithPlatform & { testInfo: TestInfo; iOSContext?: IOSTestContext }) { const prebuiltStateKey = '1user'; const appsToOpen = 2; const result = await openAppsWithState({ @@ -339,15 +373,16 @@ export async function open_Alice2({ platform, testInfo }: WithPlatform & { testI stateToBuildKey: prebuiltStateKey, groupName: undefined, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('alice2'); // we want the first user to have the first 2 devices linked - const alice = result.prebuilt.users[0]; + const alice = toUser(result.prebuilt.users[0]); const alice1 = result.devices[0]; const alice2 = result.devices[1]; - const seedPhrases = [alice.seedPhrase, alice.seedPhrase]; + const seedPhrases = [alice.recoveryPhrase, alice.recoveryPhrase]; await linkDevices(result.devices, seedPhrases); const formattedUsers: WithUsers<1> = { @@ -370,7 +405,8 @@ export async function open_Alice2({ platform, testInfo }: WithPlatform & { testI export async function open_Alice1_bob1_notfriends({ platform, testInfo, -}: WithPlatform & { testInfo: TestInfo }) { + iOSContext, +}: WithPlatform & { testInfo: TestInfo; iOSContext?: IOSTestContext }) { const appsToOpen = 2; const result = await openAppsWithState({ platform, @@ -378,16 +414,17 @@ export async function open_Alice1_bob1_notfriends({ stateToBuildKey: '2users', groupName: undefined, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); const alice1 = result.devices[0]; const bob1 = result.devices[1]; - const seedPhrases = [alice.seedPhrase, bob.seedPhrase]; + const seedPhrases = [alice.recoveryPhrase, bob.recoveryPhrase]; await linkDevices(result.devices, seedPhrases); const formattedUsers: WithUsers<2> = { @@ -408,7 +445,8 @@ export async function open_Alice2_Bob1_friends({ platform, focusFriendsConvo, testInfo, -}: WithPlatform & WithFocusFriendsConvo & { testInfo: TestInfo }) { + iOSContext, +}: WithPlatform & WithFocusFriendsConvo & { testInfo: TestInfo; iOSContext?: IOSTestContext }) { const prebuiltStateKey = '2friends'; const appsToOpen = 3; const result = await openAppsWithState({ @@ -417,14 +455,15 @@ export async function open_Alice2_Bob1_friends({ stateToBuildKey: prebuiltStateKey, groupName: undefined, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('alice2'); result.devices[2].setDeviceIdentity('bob1'); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); // we want the first user to have the first 2 devices linked - const seedPhrases = [alice.seedPhrase, alice.seedPhrase, bob.seedPhrase]; + const seedPhrases = [alice.recoveryPhrase, alice.recoveryPhrase, bob.recoveryPhrase]; await linkDevices(result.devices, seedPhrases); const alice1 = result.devices[0]; diff --git a/run/test/utils/capabilities_ios.ts b/run/test/utils/capabilities_ios.ts index 2097e9207f..aca51480b5 100644 --- a/run/test/utils/capabilities_ios.ts +++ b/run/test/utils/capabilities_ios.ts @@ -7,6 +7,11 @@ import { IntRange } from '../../types/RangeType'; dotenv.config({ quiet: true }); +export type IOSTestContext = { + customInstallTime?: string; + sessionProEnabled?: string; +}; + const iosPathPrefix = process.env.IOS_APP_PATH_PREFIX; export const iOSBundleId = 'com.loki-project.loki-messenger'; @@ -125,7 +130,7 @@ export function capabilityIsValid( export function getIosCapabilities( capabilitiesIndex: CapabilitiesIndexType, - customInstallTime?: string + customCaps?: IOSTestContext ): W3CXCUITestDriverCaps { if (capabilitiesIndex >= capabilities.length) { throw new Error( @@ -141,10 +146,14 @@ export function getIosCapabilities( const baseEnv = (caps['appium:processArguments'] as { env?: Record } | undefined)?.env ?? {}; - // Optional per-test override: - // Some tests set IOS_CUSTOM_FIRST_INSTALL_DATE_TIME before starting Appium. - // If present, inject it into the processArguments.env. Otherwise inject nothing. - const customEnv = customInstallTime ? { customFirstInstallDateTime: customInstallTime } : {}; + // Build custom env entries from per-test overrides + const customEnv: Record = {}; + if (customCaps?.customInstallTime) { + customEnv.customFirstInstallDateTime = customCaps.customInstallTime; + } + if (customCaps?.sessionProEnabled) { + customEnv.sessionPro = customCaps.sessionProEnabled; + } // Rebuild the processArguments block with merged env vars caps['appium:processArguments'] = { diff --git a/run/test/utils/click_by_coordinates.ts b/run/test/utils/click_by_coordinates.ts index 707649b88d..83320ff976 100644 --- a/run/test/utils/click_by_coordinates.ts +++ b/run/test/utils/click_by_coordinates.ts @@ -1,10 +1,8 @@ import { DeviceWrapper } from '../../types/DeviceWrapper'; import { Coordinates } from '../../types/testing'; -import { sleepFor } from './sleep_for'; export const clickOnCoordinates = async (device: DeviceWrapper, coordinates: Coordinates) => { const { x, y } = coordinates; - await sleepFor(1000); await device.pressCoordinates(x, y); device.log(`Tapped coordinates ${x}, ${y}`); }; diff --git a/run/test/utils/create_account.ts b/run/test/utils/create_account.ts index 05922ee2e8..22dbfe563a 100644 --- a/run/test/utils/create_account.ts +++ b/run/test/utils/create_account.ts @@ -10,9 +10,8 @@ import { FastModeRadio, SlowModeRadio, } from '../locators/onboarding'; -import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; +import { RecoveryPasswordMenuItem, RecoveryPhraseContainer } from '../locators/settings'; import { UserSettings } from '../locators/settings'; -import { CopyButton } from '../locators/start_conversation'; import { handleBackgroundPermissions, handleNotificationPermissions } from './permissions'; export type BaseSetupOptions = { @@ -70,18 +69,17 @@ export async function newUser( } // Open recovery phrase modal and save recovery phrase - await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); - await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + await device.clickOnElementAll(new UserSettings(device)); + await device.onIOS().scrollDown(); + await device.clickOnElementAll(new RecoveryPasswordMenuItem(device)); const recoveryPhraseContainer = await device.clickOnElementAll( new RecoveryPhraseContainer(device) ); - await device.onAndroid().clickOnElementAll(new CopyButton(device)); const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); device.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); await device.navigateBack(false); - + await device.scrollUp(); // Get Account ID from User Settings - await device.clickOnElementAll(new UserSettings(device)); const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); const accountID = await device.getTextFromElement(el); await device.clickOnElementAll(new CloseSettings(device)); diff --git a/run/test/utils/create_group.ts b/run/test/utils/create_group.ts index cbf49f2491..a5e1bc4910 100644 --- a/run/test/utils/create_group.ts +++ b/run/test/utils/create_group.ts @@ -9,7 +9,6 @@ import { CreateGroupOption } from '../locators/start_conversation'; import { newContact } from './create_contact'; import { sortByPubkey } from './get_account_id'; import { SupportedPlatformsType } from './open_app'; -import { sleepFor } from './sleep_for'; export const createGroup = async ( platform: SupportedPlatformsType, @@ -48,7 +47,6 @@ export const createGroup = async ( await device1.clickOnElementAll({ ...new Contact(device1).build(), text: userThree.userName }); // Select tick await device1.clickOnElementAll(new CreateGroupButton(device1)); - await sleepFor(3000); // Enter group chat on device 2 and 3 await Promise.all([ device2.onAndroid().navigateBack(false), @@ -76,26 +74,19 @@ export const createGroup = async ( ), ]); } - // Send message from User A to group to verify all working - await device1.sendMessage(aliceMessage); - // Did the other devices receive alice's message? - await Promise.all( - [device2, device3].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, aliceMessage)) - ) - ); - // Send message from User B to group - await device2.sendMessage(bobMessage); - await Promise.all( - [device1, device3].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, bobMessage)) - ) - ); - // Send message to User C to group - await device3.sendMessage(charlieMessage); + // Send messages from all three users simultaneously to verify group is working + await Promise.all([ + device1.sendMessage(aliceMessage), + device2.sendMessage(bobMessage), + device3.sendMessage(charlieMessage), + ]); + // Verify all messages are visible on all devices + const allMessages = [aliceMessage, bobMessage, charlieMessage]; await Promise.all( - [device1, device2].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, charlieMessage)) + [device1, device2, device3].flatMap(device => + allMessages.map(message => + device.waitForTextElementToBePresent(new MessageBody(device, message)) + ) ) ); return { userName, userOne, userTwo, userThree }; diff --git a/run/test/utils/device_registry.ts b/run/test/utils/device_registry.ts new file mode 100644 index 0000000000..6e29ba6b7c --- /dev/null +++ b/run/test/utils/device_registry.ts @@ -0,0 +1,62 @@ +import type { TestInfo } from '@playwright/test'; + +import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { getAdbFullPath } from './binaries'; +import { androidAppPackage } from './capabilities_android'; +import { SupportedPlatformsType } from './open_app'; +import { runScriptAndLog } from './utilities'; + +export type LogContext = { + startMs: number; // epoch ms — iOS: compared against log file mtime; Android: derived to epoch seconds for adb -T + pid?: string | null; // Android only — null if pidof returned nothing (app not yet running or already dead) +}; + +export type DeviceContext = { + devices: DeviceWrapper[]; + platform: SupportedPlatformsType; + logCtxByUdid?: Map; +}; + +export const deviceRegistry = new Map(); + +export function registryKey(testInfo: TestInfo): string { + return `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; +} + +// Async because Android registration fetches per-device PID for scoped logcat on failure. +export async function registerDevicesForTest( + testInfo: TestInfo, + devices: DeviceWrapper[], + platform: SupportedPlatformsType +) { + const key = registryKey(testInfo); + // Throw if registry already has an entry — indicates a previous test didn't unregister properly + if (deviceRegistry.has(key)) { + throw new Error(`Device registry already contains entry for test "${testInfo.title}"`); + } + + const startMs = Date.now(); + const logCtxByUdid = new Map(); + + if (platform === 'android') { + await Promise.all( + devices.map(async device => { + const pidOutput = await runScriptAndLog( + `${getAdbFullPath()} -s ${device.udid} shell pidof ${androidAppPackage}` + ); + const pid = pidOutput.trim() || null; + logCtxByUdid.set(device.udid, { startMs, pid }); + }) + ); + } else if (platform === 'ios') { + for (const device of devices) { + logCtxByUdid.set(device.udid, { startMs }); + } + } + + deviceRegistry.set(key, { devices, platform, logCtxByUdid }); +} + +export function unregisterDevicesForTest(testInfo: TestInfo) { + deviceRegistry.delete(registryKey(testInfo)); +} diff --git a/run/test/utils/screenshot_helper.ts b/run/test/utils/failure_artifacts.ts similarity index 57% rename from run/test/utils/screenshot_helper.ts rename to run/test/utils/failure_artifacts.ts index 9116ff257e..3fd772322e 100644 --- a/run/test/utils/screenshot_helper.ts +++ b/run/test/utils/failure_artifacts.ts @@ -1,43 +1,19 @@ import type { TestInfo } from '@playwright/test'; +import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import sharp from 'sharp'; import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { getAdbFullPath } from './binaries'; +import { iOSBundleId } from './capabilities_ios'; +import { deviceRegistry, LogContext, registryKey } from './device_registry'; import { SupportedPlatformsType } from './open_app'; +import { runScriptAndLog } from './utilities'; -// Screenshot context type -type ScreenshotContext = { - devices: DeviceWrapper[]; - testInfo: TestInfo; - platform: SupportedPlatformsType; -}; - -// Global registry to track devices for screenshot capture -const deviceRegistry = new Map(); - -// Register devices for a test -export function registerDevicesForTest( - testInfo: TestInfo, - devices: DeviceWrapper[], - platform: SupportedPlatformsType -) { - const testId = `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; - // Throw if deviceRegistry already has an entry for this test - // Could indicate that previous test did not unregister properly - if (deviceRegistry.has(testId)) { - throw new Error(`Device registry already contains entry for test "${testInfo.title}"`); - } - - deviceRegistry.set(testId, { devices, testInfo, platform }); -} +// --- Screenshots --- -// Unregister devices after test -export function unregisterDevicesForTest(testInfo: TestInfo) { - const testId = `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; - deviceRegistry.delete(testId); -} // Add device labels to screenshots (e.g. "Device: alice1") async function addDeviceLabel(screenshot: Buffer, device: DeviceWrapper): Promise { const { width } = await sharp(screenshot).metadata(); @@ -50,11 +26,11 @@ async function addDeviceLabel(screenshot: Buffer, device: DeviceWrapper): Promis - Device: ${deviceName} @@ -63,14 +39,7 @@ async function addDeviceLabel(screenshot: Buffer, device: DeviceWrapper): Promis // Composite label over screenshot return sharp(screenshot) - .composite([ - { - input: label, - top: 0, - left: 0, - blend: 'over', - }, - ]) + .composite([{ input: label, top: 0, left: 0, blend: 'over' }]) .png() .toBuffer(); } @@ -116,14 +85,7 @@ async function createComposite(screenshots: Buffer[]): Promise { const composites = screenshots.map((screenshot, index) => { const col = index % cols; const row = Math.floor(index / cols); - const x = col * (width + gap); - const y = row * (height + gap); - - return { - input: screenshot, - left: x, - top: y, - }; + return { input: screenshot, left: col * (width + gap), top: row * (height + gap) }; }); // Apply all screenshots to canvas @@ -132,8 +94,7 @@ async function createComposite(screenshots: Buffer[]): Promise { // Main screenshot capture function export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise { - const testId = `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; - const context = deviceRegistry.get(testId); + const context = deviceRegistry.get(registryKey(testInfo)); if (!context || context.devices.length === 0) { console.log('No devices registered for screenshot capture'); @@ -143,30 +104,20 @@ export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise { try { const base64 = await device.getScreenshot(); - return { - device, - base64, - success: true, - }; + return { device, base64, success: true }; } catch (error) { console.error(`Failed to capture from ${device.getDeviceIdentity()}:`, error); - return { - device, - base64: null, - success: false, - }; + return { device, base64: null, success: false }; } }) ); // Filter out failed captures const successfulCaptures = rawCaptures.filter(c => c.success && c.base64); - if (successfulCaptures.length === 0) { console.log('No screenshots captured successfully'); return; @@ -180,16 +131,19 @@ export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise => { - if (result.status === 'rejected') { - console.error(`Failed to process screenshot:`, result.reason); - return false; + .filter( + ( + result + ): result is PromiseFulfilledResult<{ device: DeviceWrapper; labeledBuffer: Buffer }> => { + if (result.status === 'rejected') { + console.error(`Failed to process screenshot:`, result.reason); + return false; + } + return true; } - return true; - }) + ) .map(result => result.value.labeledBuffer); if (screenshots.length === 0) { @@ -198,7 +152,6 @@ export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise { + if (platform === 'android') { + const startEpochSec = (logCtx.startMs / 1000).toFixed(3); + const parts = [ + `${getAdbFullPath()} -s ${device.udid} logcat -d -T ${startEpochSec}`, + ...(logCtx.pid ? [`--pid=${logCtx.pid}`] : []), + ]; + const output = await runScriptAndLog(parts.join(' ')); + return Buffer.from(output); + } + + if (platform === 'ios') { + const containerPath = execSync( + `xcrun simctl get_app_container ${device.udid} ${iOSBundleId} data`, + { encoding: 'utf8' } + ).trim(); + + const logsDir = path.join(containerPath, 'Library', 'Caches', 'Logs'); + + if (!fs.existsSync(logsDir)) { + console.log(`No logs directory found for ${device.getDeviceIdentity()}`); + return null; + } + + const logFiles = fs + .readdirSync(logsDir) + .filter(f => f.startsWith(iOSBundleId) && f.endsWith('.log')) + .map(f => ({ name: f, mtime: fs.statSync(path.join(logsDir, f)).mtimeMs })) + .filter(f => f.mtime >= logCtx.startMs) + .sort((a, b) => b.mtime - a.mtime); + + if (logFiles.length === 0) { + console.log(`No log files found after test start for ${device.getDeviceIdentity()}`); + return null; + } + + return Buffer.from(fs.readFileSync(path.join(logsDir, logFiles[0].name), 'utf8')); + } + + return null; +} + +const MAX_LOG_BYTES = 2 * 1024 * 1024; // 2 MB — tail beyond this to keep reports lean + +function tailBuffer(raw: Buffer): Buffer { + if (raw.length <= MAX_LOG_BYTES) return raw; + + const tail = raw.subarray(raw.length - MAX_LOG_BYTES); + // Advance past any partial line at the cut point + const firstNewline = tail.indexOf('\n'.charCodeAt(0)); + return firstNewline > 0 ? tail.subarray(firstNewline + 1) : tail; +} + +export async function captureLogsOnFailure(testInfo: TestInfo): Promise { + const context = deviceRegistry.get(registryKey(testInfo)); + + if (!context?.logCtxByUdid) { + return; + } + + await Promise.all( + context.devices.map(async device => { + const logCtx = context.logCtxByUdid!.get(device.udid); + if (!logCtx) return; + + try { + const raw = await collectLogBuffer(context.platform, device, logCtx); + if (!raw) return; + + const buffer = tailBuffer(raw); + const label = device.getDeviceIdentity(); + const truncated = raw.length !== buffer.length; + await testInfo.attach(`device-log-${label}`, { body: buffer, contentType: 'text/plain' }); + console.log( + `Log captured for ${label} (${buffer.length} bytes${truncated ? `, truncated from ${raw.length}` : ''})` + ); + } catch (error) { + console.error(`Failed to capture log for ${device.getDeviceIdentity()}:`, error); + } + }) + ); +} diff --git a/run/test/utils/get_account_id.ts b/run/test/utils/get_account_id.ts index 31f1b3b187..1c9a7bd2a3 100644 --- a/run/test/utils/get_account_id.ts +++ b/run/test/utils/get_account_id.ts @@ -1,16 +1,10 @@ -import { StateUser } from '@session-foundation/qa-seeder'; - import { User } from '../../types/testing'; import { SupportedPlatformsType } from './open_app'; -// Sorts users by pubkey hex (StateUser.sessionId from qa-seeder or User.accountID from local types) and returns usernames -export function sortByPubkey(...users: Array) { +// Sorts users by pubkey hex and returns their usernames +export function sortByPubkey(...users: Array) { return [...users] - .sort((a, b) => { - const aKey = 'accountID' in a ? a.accountID : String(a.sessionId); - const bKey = 'accountID' in b ? b.accountID : String(b.sessionId); - return aKey.localeCompare(bKey); - }) + .sort((a, b) => a.accountID.localeCompare(b.accountID)) .map(user => user.userName); } diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts new file mode 100644 index 0000000000..cd8a2a2c16 --- /dev/null +++ b/run/test/utils/mock_pro.ts @@ -0,0 +1,386 @@ +/** + * Session Pro Test Account Setup + * + * Registers test accounts as Pro subscribers against the Session Pro dev backend, + * bypassing Google Play / Apple App Store verification entirely. + * + * Based on: + * https://github.com/session-foundation/session-pro-backend/blob/main/examples/endpoint_example.py + * + * Usage: + * import { makeAccountPro } from './mock_pro'; + * + * await makeAccountPro({ user: alice, platform }); + * + * In order for the changes to take effect in the clients it's best to force close and restart the app + */ + +import { ed25519 } from '@noble/curves/ed25519.js'; +import { blake2b } from '@noble/hashes/blake2.js'; +import { randomBytes } from 'crypto'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { PRO_BACKEND_URL } from '../../constants'; +import { User } from '../../types/testing'; +import { SupportedPlatformsType } from './open_app'; + +type PaymentProvider = 'apple' | 'google'; + +type MakeAccountProParams = { + user: User; + platform: SupportedPlatformsType; + dryRun?: boolean; // If true, build and print the request but don't send it +}; + +type AddProPaymentRequest = { + version: number; + master_pkey: string; + rotating_pkey: string; + master_sig: string; + rotating_sig: string; + payment_tx: { + provider: number; + google_payment_token?: string; + google_order_id?: string; + apple_tx_id?: string; + }; +}; + +type ProProof = { + version: number; + expiry_unix_ts_ms: number; + gen_index_hash: string; + rotating_pkey: string; + sig: string; +}; + +type AddProPaymentResponse = { + status: number; + result?: ProProof; + errors?: string[]; +}; + +let WORDLIST_CACHE: string[] | null = null; + +function getWordlist(): string[] { + if (WORDLIST_CACHE) { + return WORDLIST_CACHE; + } + + const wordlistPath = join(__dirname, '../../../english_wordlist.txt'); + const content = readFileSync(wordlistPath, 'utf-8'); + const words = content + .split('\n') + .map(w => w.trim()) + .filter(Boolean); + + if (words.length !== 1626) { + throw new Error(`Expected 1626 words in wordlist, got ${words.length}`); + } + + WORDLIST_CACHE = words; + return words; +} + +// Decodes a 13-word recovery phrase to a 16-byte seed hex string. */ +function mnemonicToSeedHex(mnemonic: string): string { + const wordlist = getWordlist(); + const n = wordlist.length; // 1626 + + const words = mnemonic.toLowerCase().trim().split(/\s+/); + if (words.length !== 13) { + throw new Error(`Expected 13 words, got ${words.length}`); + } + + // Build word -> index lookup + const wordToIdx = new Map(); + wordlist.forEach((w, i) => wordToIdx.set(w, i)); + + // Resolve word indices (with prefix matching support) + const indices: number[] = []; + for (const word of words) { + if (wordToIdx.has(word)) { + indices.push(wordToIdx.get(word)!); + } else { + // Try prefix match (first 4 chars) + const matches = wordlist + .map((w, i) => ({ w, i })) + .filter(({ w }) => w.startsWith(word.slice(0, 4))); + + if (matches.length === 1) { + indices.push(matches[0].i); + } else { + throw new Error(`Unknown or ambiguous mnemonic word: '${word}'`); + } + } + } + + // Decode: every 3 words -> 4 bytes (little-endian) + const dataIndices = indices.slice(0, 12); + const seedBytes: number[] = []; + for (let i = 0; i < 12; i += 3) { + const w1 = dataIndices[i]; + const w2 = dataIndices[i + 1]; + const w3 = dataIndices[i + 2]; + + const x = w1 + n * ((((w2 - w1) % n) + n) % n) + n * n * ((((w3 - w2) % n) + n) % n); + + // Convert to 4 bytes little-endian + seedBytes.push(x & 0xff); + seedBytes.push((x >> 8) & 0xff); + seedBytes.push((x >> 16) & 0xff); + seedBytes.push((x >> 24) & 0xff); + } + + if (seedBytes.length !== 16) { + throw new Error(`Expected 16 bytes, got ${seedBytes.length}`); + } + + return Buffer.from(seedBytes).toString('hex'); +} + +function padSeed(seedHex: string): Uint8Array { + const seed = Buffer.from(seedHex, 'hex'); + if (seed.length !== 16) { + throw new Error(`Seed must be 16 bytes, got ${seed.length}`); + } + + // Pad with 16 zero bytes + const padded = new Uint8Array(32); + padded.set(seed, 0); + return padded; +} + +// Derives the Pro master keypair from the seed using Blake2b with "SessionProRandom" as the key. +function deriveProMasterKey(seedHex: string): { + privateKey: Uint8Array; + publicKey: Uint8Array; +} { + const padded = padSeed(seedHex); + + // Blake2b-256 with "SessionProRandom" as the key + const proSeed = blake2b(padded, { + dkLen: 32, + key: Buffer.from('SessionProRandom', 'utf-8'), + }); + + const privateKey = proSeed; + const publicKey = ed25519.getPublicKey(privateKey); + + return { privateKey, publicKey }; +} + +// Generates a random ephemeral rotating keypair for the payment request. +function generateRotatingKey(): { privateKey: Uint8Array; publicKey: Uint8Array } { + const privateKey = ed25519.utils.randomSecretKey(); + const publicKey = ed25519.getPublicKey(privateKey); + return { privateKey, publicKey }; +} + +function makeAddProPaymentHash( + version: number, + masterPubkey: Uint8Array, + rotatingPubkey: Uint8Array, + provider: number, + paymentToken?: string, + orderId?: string, + appleTxId?: string +): Uint8Array { + const personalization = Buffer.from('ProAddPayment___', 'utf-8'); // 16 bytes + + const parts: Uint8Array[] = [ + new Uint8Array([version]), + masterPubkey, + rotatingPubkey, + new Uint8Array([provider]), + ]; + + if (provider === 1) { + // Google + if (!paymentToken || !orderId) { + throw new Error('Google provider requires payment_token and order_id'); + } + parts.push(Buffer.from(paymentToken, 'utf-8')); + parts.push(Buffer.from(orderId, 'utf-8')); + } else if (provider === 2) { + // Apple + if (!appleTxId) { + throw new Error('Apple provider requires tx_id'); + } + parts.push(Buffer.from(appleTxId, 'utf-8')); + } + + // Concatenate all parts + const totalLen = parts.reduce((sum, p) => sum + p.length, 0); + const message = new Uint8Array(totalLen); + let offset = 0; + for (const part of parts) { + message.set(part, offset); + offset += part.length; + } + + return blake2b(message, { dkLen: 32, personalization }); +} + +// Builds a signed add_pro_payment request body with fake payment tokens. +function buildAddProPaymentRequest( + masterKey: { privateKey: Uint8Array; publicKey: Uint8Array }, + rotatingKey: { privateKey: Uint8Array; publicKey: Uint8Array }, + provider: PaymentProvider +): AddProPaymentRequest { + const version = 0; + const providerNum = provider === 'google' ? 1 : 2; + + let paymentToken: string | undefined; + let orderId: string | undefined; + let appleTxId: string | undefined; + + const timestamp = Date.now(); + const nonce = randomBytes(4).toString('hex'); + + if (provider === 'google') { + paymentToken = `DEV.${timestamp}.${nonce}`; + orderId = `DEV.${timestamp}.${nonce}`; + } else { + appleTxId = `DEV.${timestamp}.${nonce}`; + } + + const hash = makeAddProPaymentHash( + version, + masterKey.publicKey, + rotatingKey.publicKey, + providerNum, + paymentToken, + orderId, + appleTxId + ); + + const masterSig = ed25519.sign(hash, masterKey.privateKey); + const rotatingSig = ed25519.sign(hash, rotatingKey.privateKey); + + const paymentTx: AddProPaymentRequest['payment_tx'] = { + provider: providerNum, + }; + + if (provider === 'google') { + paymentTx.google_payment_token = paymentToken; + paymentTx.google_order_id = orderId; + } else { + paymentTx.apple_tx_id = appleTxId; + } + + return { + version, + master_pkey: Buffer.from(masterKey.publicKey).toString('hex'), + rotating_pkey: Buffer.from(rotatingKey.publicKey).toString('hex'), + master_sig: Buffer.from(masterSig).toString('hex'), + rotating_sig: Buffer.from(rotatingSig).toString('hex'), + payment_tx: paymentTx, + }; +} + +// POSTs the payment request to the Pro backend with retries and timeout. +async function addProPayment( + backendUrl: string, + request: AddProPaymentRequest, + { maxAttempts = 3, timeout = 10_000 } = {} +): Promise { + const url = `${backendUrl}/add_pro_payment`; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + const data = (await response.json()) as AddProPaymentResponse; + + if (!response.ok || data.status !== 0) { + throw new Error( + `Failed to add Pro payment: ${data.errors?.join(', ') || `HTTP ${response.status}`}` + ); + } + + return data; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + if (attempt === maxAttempts) { + throw new Error(`add_pro_payment failed after ${maxAttempts} attempts: ${msg}`); + } + console.log(`add_pro_payment attempt ${attempt}/${maxAttempts} failed: ${msg}, retrying...`); + } + } + + throw new Error('Unreachable'); +} + +// Registers a test account as a Pro subscriber against the dev backend. +export async function makeAccountPro(params: MakeAccountProParams): Promise { + const { user, platform, dryRun = false } = params; + const mnemonic = user.recoveryPhrase; + const provider: PaymentProvider = platform === 'ios' ? 'apple' : 'google'; + const seedHex = mnemonicToSeedHex(mnemonic); + const masterKey = deriveProMasterKey(seedHex); + const rotatingKey = generateRotatingKey(); + // Build request + const request = buildAddProPaymentRequest(masterKey, rotatingKey, provider); + console.log(`\nRequest body: + ${JSON.stringify(request, null, 2)}`); + + if (dryRun) { + console.log('\nDRY RUN - Request not sent'); + return null; + } + + // Send request + console.log(`\nSending request to ${PRO_BACKEND_URL}...`); + const response = await addProPayment(PRO_BACKEND_URL, request); + + if (!response.result) { + throw new Error('No proof in response'); + } + + console.log('Account successfully registered as Pro'); + console.log(` Expiry: ${new Date(response.result.expiry_unix_ts_ms).toISOString()}`); + + return response.result; +} + +if (require.main === module) { + const args = process.argv.slice(2); + + if (args.length < 2) { + console.error( + 'Usage: npx ts-node run/test/utils/mock_pro.ts [--dry-run]' + ); + console.error('Example: npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." android'); + console.error( + ' npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." ios --dry-run' + ); + process.exit(1); + } + + const dryRun = args.includes('--dry-run'); + const filteredArgs = args.filter(a => a !== '--dry-run'); + const [mnemonic, platform] = filteredArgs; + + makeAccountPro({ + user: { userName: '' as any, accountID: '', recoveryPhrase: mnemonic }, + platform: platform as SupportedPlatformsType, + dryRun, + }) + .then(() => process.exit(0)) + .catch(err => { + console.error('Error:', err.message); + process.exit(1); + }); +} diff --git a/run/test/utils/open_app.ts b/run/test/utils/open_app.ts index fa2e37e83b..cbc9c96c35 100644 --- a/run/test/utils/open_app.ts +++ b/run/test/utils/open_app.ts @@ -14,9 +14,10 @@ import { capabilityIsValid, getIosCapabilities, iOSBundleId, + IOSTestContext, } from './capabilities_ios'; +import { registerDevicesForTest } from './device_registry'; import { cleanPermissions } from './permissions'; -import { registerDevicesForTest } from './screenshot_helper'; import { sleepFor } from './sleep_for'; import { runScriptAndLog } from './utilities'; @@ -24,10 +25,6 @@ const APPIUM_PORT = 4728; export type SupportedPlatformsType = 'android' | 'ios'; -export type IOSTestContext = { - customInstallTime?: string; -}; - export const openAppMultipleDevices = async ( platform: SupportedPlatformsType, numberOfDevices: number, @@ -45,7 +42,7 @@ export const openAppMultipleDevices = async ( // Map the result to return only the device objects const devices = apps.map(app => app.device); - registerDevicesForTest(testInfo, devices, platform); + await registerDevicesForTest(testInfo, devices, platform); return devices; }; @@ -73,7 +70,7 @@ export const openAppOnPlatformSingleDevice = async ( }> => { const result = await openAppOnPlatform(platform, 0, testInfo, iOSContext); - registerDevicesForTest(testInfo, [result.device], platform); + await registerDevicesForTest(testInfo, [result.device], platform); return result; }; @@ -93,7 +90,7 @@ export const openAppTwoDevices = async ( const result = { device1: app1.device, device2: app2.device }; - registerDevicesForTest(testInfo, Object.values(result), platform); + await registerDevicesForTest(testInfo, Object.values(result), platform); return result; }; @@ -119,7 +116,7 @@ export const openAppThreeDevices = async ( device3: app3.device, }; - registerDevicesForTest(testInfo, Object.values(result), platform); + await registerDevicesForTest(testInfo, Object.values(result), platform); return result; }; @@ -148,7 +145,7 @@ export const openAppFourDevices = async ( device4: app4.device, }; - registerDevicesForTest(testInfo, Object.values(result), platform); + await registerDevicesForTest(testInfo, Object.values(result), platform); return result; }; @@ -311,7 +308,7 @@ const openiOSApp = async ( const capabilities = getIosCapabilities( actualCapabilitiesIndex as CapabilitiesIndexType, - iOSContext?.customInstallTime + iOSContext ); const udid = capabilities.alwaysMatch['appium:udid'] as string; diff --git a/run/test/utils/restore_account.ts b/run/test/utils/restore_account.ts index ae93f62d8d..26b7c95923 100644 --- a/run/test/utils/restore_account.ts +++ b/run/test/utils/restore_account.ts @@ -82,7 +82,6 @@ export const restoreAccountNoFallback = async ( // Wait for permissions modal to pop up await sleepFor(500); await handleNotificationPermissions(device, allowNotificationPermissions); - await sleepFor(1000); // Check that we're on the home screen await device.waitForTextElementToBePresent(new PlusButton(device)); }; diff --git a/run/test/utils/set_disappearing_messages.ts b/run/test/utils/set_disappearing_messages.ts index b35ad1b257..29683962ad 100644 --- a/run/test/utils/set_disappearing_messages.ts +++ b/run/test/utils/set_disappearing_messages.ts @@ -6,52 +6,21 @@ import { DisappearingMessagesMenuOption, DisappearingMessagesSubtitle, DisappearingMessagesTimerType, - FollowSettingsButton, SetDisappearMessagesButton, - SetModalButton, } from '../locators/disappearing_messages'; -import { SupportedPlatformsType } from './open_app'; -import { sleepFor } from './sleep_for'; export const setDisappearingMessage = async ( - platform: SupportedPlatformsType, device: DeviceWrapper, - [conversationType, timerType, timerDuration = DISAPPEARING_TIMES.THIRTY_SECONDS]: MergedOptions, - device2?: DeviceWrapper + [conversationType, timerType, timerDuration = DISAPPEARING_TIMES.THIRTY_SECONDS]: MergedOptions ) => { const enforcedType: ConversationType = conversationType; await device.clickOnElementAll(new ConversationSettings(device)); - // Wait for UI to load conversation options menu - await sleepFor(500); await device.clickOnElementAll(new DisappearingMessagesMenuOption(device)); if (enforcedType === '1:1') { await device.clickOnElementAll(new DisappearingMessagesTimerType(device, timerType)); } - if (timerType === 'Disappear after read option') { - if (enforcedType === '1:1') { - await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.TWELVE_HOURS); - } else { - await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); - } - } else if ( - enforcedType === 'Group' || - (enforcedType === 'Note to Self' && timerType === 'Disappear after send option') - ) { - await device.onIOS().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_IOS); - await device.onAndroid().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_ANDROID); - } else { - await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); - } - await device.clickOnElementAll(new DisappearingMessageRadial(device, timerDuration)); await device.clickOnElementAll(new SetDisappearMessagesButton(device)); await device.navigateBack(); - // Extended the wait for the Follow settings button to settle in the UI, it was moving and confusing appium - await sleepFor(2000); - if (device2) { - await device2.clickOnElementAll(new FollowSettingsButton(device2)); - await sleepFor(500); - await device2.clickOnElementAll(new SetModalButton(device2)); - } await device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)); }; diff --git a/run/test/utils/test_setup.ts b/run/test/utils/test_setup.ts deleted file mode 100644 index 97af4b9edf..0000000000 --- a/run/test/utils/test_setup.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -Checkout branch that needs testing: -The Command needs to include: -- platform -- branch -- number of emulators ( new user to old user ratio ) - - -navigate to platform folder - Documents > session-(platform) -git checkout *branch* -then build from branch (ios does automatically, android requires manually click on hammer icon) - -ANDROID -start two emulators (cold boot on android) -cd ~/Library/Android/sdk -./emulator/emulator -avd Pixel_4_API_30 - -open new terminal window -cd ~/Library/Android/sdk -./emulator/emulator -avd Pixel_4_API_30_2 - -IOS -open -a simulator --args -IOS_1_SIMULATOR -no-boot-anim -open -a simulator --args -IOS_2_SIMULATOR -no-boot-anim - -run branch on emulator one -run branch on emulator two - -once session is running on emulator one: -click create session id -save session id -click continue -enter display name -continue -continue -continue -save recovery phrase from reminder -navigate out of reminder page - -once session is running on emulator two: -log into test account -click on restore account - - - - - - - -*/ diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index e0461f1d70..c21434f910 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -5,6 +5,7 @@ import path from 'path'; import * as util from 'util'; import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { PlusButton } from '../locators/home'; import { androidAppActivity, androidAppPackage } from './capabilities_android'; import { iOSBundleId } from './capabilities_ios'; import { sleepFor } from './sleep_for'; @@ -146,4 +147,6 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise await runScriptAndLog(`xcrun simctl launch ${device.udid} ${iOSBundleId}`, true); await sleepFor(1_000); } + // Ensure we're on the home screen again + await device.waitForTextElementToBePresent(new PlusButton(device)); } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index e52826903e..73c7fa47ce 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1,7 +1,7 @@ import type { Constraints, DefaultCreateSessionResult } from '@appium/types'; import { getImageOccurrence } from '@appium/opencv'; -import { TestInfo } from '@playwright/test'; +import { expect, TestInfo } from '@playwright/test'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; import { W3CUiautomator2DriverCaps } from 'appium-uiautomator2-driver/build/lib/types'; import { W3CXCUITestDriverCaps, XCUITestDriver } from 'appium-xcuitest-driver/build/lib/driver'; @@ -18,12 +18,14 @@ import { describeLocator, DownloadMediaButton, FirstGif, + GIFName, ImageName, ImagePermissionsModalAllow, LocatorsInterface, ReadReceiptsButton, } from '../../run/test/locators'; import { + animatedProfilePicture, profilePicture, testFile, testImage, @@ -75,10 +77,11 @@ import { parseDataImage } from '../test/utils/check_colour'; import { isSameColor } from '../test/utils/check_colour'; import { SupportedPlatformsType } from '../test/utils/open_app'; import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/utils/utilities'; +import { CTAConfig, ctaConfigs, CTAType } from './cta'; import { AccessibilityId, + Coordinates, DISAPPEARING_TIMES, - Group, Id, InteractionPoints, Strategy, @@ -87,10 +90,6 @@ import { XPath, } from './testing'; -export type Coordinates = { - x: number; - y: number; -}; export type ActionSequence = { actions: string; }; @@ -571,6 +570,16 @@ export class DeviceWrapper { return []; } + private resolveLocator(args: LocatorsInterface | (StrategyExtractionObj & { text?: string })): { + locator: StrategyExtractionObj; + description: string; + } { + const built = args instanceof LocatorsInterface ? args.build() : args; + const text = args instanceof LocatorsInterface ? undefined : args.text; + const locator = text ? { ...built, text } : built; + return { locator, description: describeLocator(locator) }; + } + /** * Attempts to find an element using a primary locator, and if not found, falls back to a secondary locator. * This is useful for supporting UI transitions (e.g., between legacy and Compose Android screens) where @@ -588,13 +597,10 @@ export class DeviceWrapper { fallbackLocator: LocatorsInterface | StrategyExtractionObj, maxWait: number = 3000 ): Promise { - const primary = - primaryLocator instanceof LocatorsInterface ? primaryLocator.build() : primaryLocator; - const fallback = - fallbackLocator instanceof LocatorsInterface ? fallbackLocator.build() : fallbackLocator; - - const primaryDescription = describeLocator(primary); - const fallbackDescription = describeLocator(fallback); + const { locator: primary, description: primaryDescription } = + this.resolveLocator(primaryLocator); + const { locator: fallback, description: fallbackDescription } = + this.resolveLocator(fallbackLocator); try { return await this.waitForTextElementToBePresent({ ...primary, maxWait, skipHealing: true }); @@ -617,18 +623,47 @@ export class DeviceWrapper { } } - public async longClick(element: AppiumNextElementType, durationMs: number) { + // Appium taps elements in their center but sometimes that is not desirable + // The native methods apply the tap offset from the top left corner + // For a more intuitive offset calculation, this method allows us to + // define offsets based on the element center + private async calculateGestureOffset( + element: AppiumNextElementType, + offset: Coordinates + ): Promise { + const rect = await this.getElementRect(element.ELEMENT); + if (!rect) { + throw new Error('Failed to resolve element rect for offset calculation'); + } + const { width, height } = rect; + const centerX = Math.round(width / 2); + const centerY = Math.round(height / 2); + // Clamp offset to element bounds + const x = Math.min(Math.max(centerX + offset.x, 0), rect.width); + const y = Math.min(Math.max(centerY + offset.y, 0), rect.height); + return { x, y }; + } + + /** + * @param offset Pixel offset from the element center. + * If an offset is necessary, both x and y must be defined, otherwise Appium doesn't apply the offset parameter. + */ + public async longClick(element: AppiumNextElementType, durationMs: number, offset?: Coordinates) { + let xOffset: number | undefined; + let yOffset: number | undefined; + + if (offset) { + const offsetCoordinates = await this.calculateGestureOffset(element, offset); + xOffset = offsetCoordinates.x; + yOffset = offsetCoordinates.y; + } + if (this.isIOS()) { // iOS takes a number in seconds const duration = Math.floor(durationMs / 1000); - return this.toIOS().mobileTouchAndHold(duration, undefined, undefined, element.ELEMENT); + return this.toIOS().mobileTouchAndHold(duration, xOffset, yOffset, element.ELEMENT); } - return this.toAndroid().mobileLongClickGesture( - element.ELEMENT, - undefined, - undefined, - durationMs - ); + return this.toAndroid().mobileLongClickGesture(element.ELEMENT, xOffset, yOffset, durationMs); } public async clickOnByAccessibilityID( @@ -742,32 +777,31 @@ export class DeviceWrapper { * @throws if message not found or context menu fails to appear within maxWait */ public async longPressMessage( - args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) + args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), + options?: { offset?: Coordinates } ): Promise { - const { text, maxWait = 10_000 } = args; - const locator = args instanceof LocatorsInterface ? args.build() : args; - - // Merge text if provided - const finalLocator = text ? { ...locator, text } : locator; + const { maxWait = 10_000 } = args; + const { locator, description } = this.resolveLocator(args); - const displayText = describeLocator(finalLocator); - this.log(`Attempting long press on ${displayText}`); + this.log(`Attempting long press on ${description}`); await this.pollUntil( async () => { // Find the message - this.log(`Looking for: ${JSON.stringify(finalLocator)}`); + this.log(`Looking for: ${JSON.stringify(locator)}`); const el = await this.waitForTextElementToBePresent({ - ...finalLocator, + ...locator, maxWait: 1_000, }); if (!el) { - return { success: false, error: `Message not found: ${displayText}` }; + return { success: false, error: `Message not found: ${description}` }; + } + if (options?.offset) { + this.log(`Offsetting long press by x=${options?.offset?.x}, y=${options?.offset?.y}`); } - // Attempt long click - await this.longClick(el, 2000); + await this.longClick(el, 2000, options?.offset); // Check if context menu appeared const longPressSuccess = await this.waitForTextElementToBePresent({ @@ -783,7 +817,7 @@ export class DeviceWrapper { return { success: false, - error: `Long press didn't show context menu for ${displayText}`, + error: `Long press didn't show context menu for ${description}`, }; }, { @@ -1049,134 +1083,119 @@ export class DeviceWrapper { const message = await this.findMatchingTextAndAccessibilityId('Message body', textToLookFor); return message; } + /** - * Attempts to visually match a reference image against all elements found by the given locator, - * and taps the best match (or the first high-confidence match if earlyMatch is enabled). - * This is useful for scenarios where UI elements cannot be reliably identified, - * such as elements with date-based accessibility IDs. + * Attempts to visually match a reference image against all instances found by the given locator, and taps the best match. + * All element screenshots are taken in parallel. + * If the method finds 0 results for a locator, retries with exponential backoff up to 5 seconds. * * @param locator - The strategy and selector to find candidate elements. * @param referenceImageName - The filename of the reference image (in the media directory). - * @param earlyMatch - If true, taps immediately on the first match above the earlyMatchThreshold. * @throws If no suitable match is found among the candidate elements. */ public async matchAndTapImage( locator: StrategyExtractionObj, - referenceImageName: string, - earlyMatch: boolean = true + referenceImageName: string ): Promise { const threshold = 0.85; - const earlyMatchThreshold = 0.97; - // Find all candidate elements matching the locator - const elements = await this.findElements(locator.strategy, locator.selector); + // Retry findElements with exponential backoff — photo picker may not have rendered yet + let elements = await this.findElements(locator.strategy, locator.selector); + if (elements.length === 0) { + let delay = 100; + const maxWait = 5000; + const start = Date.now(); + while (elements.length === 0 && Date.now() - start < maxWait) { + await sleepFor(delay); + delay = Math.min(delay * 2, 1600); + elements = await this.findElements(locator.strategy, locator.selector); + } + } + this.info( `[matchAndTapImage] Starting image matching: ${elements.length} elements with ${locator.strategy} "${locator.selector}"` ); - // Load the reference image buffer from disk + // Load the reference image buffer from disk once const referencePath = path.join('run', 'test', 'media', referenceImageName); await fs.access(referencePath).catch(() => { throw new Error(`Reference image not found: ${referencePath}`); }); const referenceBuffer = await fs.readFile(referencePath); + // Reference metadata never changes across elements + const refMeta = await sharp(referenceBuffer).metadata(); - let bestMatch: { - center: { x: number; y: number }; - score: number; - } | null = null; - - // Iterate over each candidate element - for (const el of elements) { - // Take a screenshot of the element - const base64 = await this.getElementScreenshot(el.ELEMENT); - const elementBuffer = Buffer.from(base64, 'base64'); + // Phase 1: screenshot + comparison in parallel + const results = await Promise.all( + elements.map(async el => { + const base64 = await this.getElementScreenshot(el.ELEMENT); + const elementBuffer = Buffer.from(base64, 'base64'); - // Get the element's rectangle (position and size) - const rect = await this.getElementRect(el.ELEMENT); - if (!rect) { - continue; - } - // Get actual pixel dimensions of the element screenshot - const elementMeta = await sharp(elementBuffer).metadata(); - // Get original reference image dimensions - const refMeta = await sharp(referenceBuffer).metadata(); + const elementMeta = await sharp(elementBuffer).metadata(); - let resizedRef: Buffer; + let resizedRef: Buffer; + let resizedMeta: Awaited>; - if (elementMeta.width === refMeta.width && elementMeta.height === refMeta.height) { - // Skip resizing if reference already matches the screenshot dimensions - resizedRef = referenceBuffer; - } else { - // Resize the reference image to exactly match the screenshot dimensions - const targetWidth = elementMeta.width; - const targetHeight = elementMeta.height; + if (elementMeta.width === refMeta.width && elementMeta.height === refMeta.height) { + // Skip resizing if reference already matches the screenshot dimensions + resizedRef = referenceBuffer; + resizedMeta = refMeta; + } else { + resizedRef = await sharp(referenceBuffer) + .resize(elementMeta.width, elementMeta.height) + .toBuffer(); + resizedMeta = await sharp(resizedRef).metadata(); + } - resizedRef = await sharp(referenceBuffer).resize(targetWidth, targetHeight).toBuffer(); - } + try { + const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { + threshold, + }); + return { el, matchRect, score, resizedMeta }; + } catch { + return null; + } + }) + ); - try { - const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { - threshold, - }); + type MatchResult = NonNullable<(typeof results)[number]>; + const bestResult = results + .filter((r): r is MatchResult => r !== null) + .reduce((best, r) => (!best || r.score > best.score ? r : best), null); - /** - * Matching is done on a resized reference image to account for device pixel density. - * However, the coordinates returned by getImageOccurrence are relative to the resized buffer, - * *not* the original screen element. This leads to incorrect tap positions unless we - * scale the match result back down to the actual dimensions of the element. - * The logic below handles this scaling correction, ensuring the tap lands at the correct - * screen coordinates — even when Retina displays and image resizing are involved. - */ - - // Calculate scale between resized image and element dimensions - const resizedMeta = await sharp(resizedRef).metadata(); - const scaleX = rect.width / (resizedMeta.width ?? rect.width); - const scaleY = rect.height / (resizedMeta.height ?? rect.height); - - // Calculate center of the match rectangle (in buffer space) - const matchCenterX = matchRect.x + Math.floor(matchRect.width / 2); - const matchCenterY = matchRect.y + Math.floor(matchRect.height / 2); - - // Scale match center down to element space - const scaledCenterX = matchCenterX * scaleX; - const scaledCenterY = matchCenterY * scaleY; - - // Final absolute coordinates - const tapX = Math.round(rect.x + scaledCenterX); - const tapY = Math.round(rect.y + scaledCenterY); - - const center = { x: tapX, y: tapY }; - - // If earlyMatch is enabled and the score is high enough, tap immediately - if (earlyMatch && score >= earlyMatchThreshold) { - this.info( - `[matchAndTapImage] Tapping first high-confidence match (${(score * 100).toFixed(2)}%)` - ); - await clickOnCoordinates(this, center); - return; - } - // Otherwise, keep track of the best match so far - if (!bestMatch || score > bestMatch.score) { - bestMatch = { center, score }; - } - } catch { - continue; // No match in this element, try next - } - } - // If no good match was found, throw an error - if (!bestMatch) { + if (!bestResult) { console.log( `[matchAndTapImage] No matching image found among ${elements.length} elements for ${locator.strategy} "${locator.selector}"` ); throw new Error('Unable to find the expected UI element on screen'); } - // Tap the best match found - this.info( - `[matchAndTapImage] Tapping best match with ${(bestMatch.score * 100).toFixed(2)}% confidence` - ); - await clickOnCoordinates(this, bestMatch.center); + + // Phase 2: fetch rect only for the winning element to determine tap coords + const rect = await this.getElementRect(bestResult.el.ELEMENT); + + if (!rect) { + throw new Error('Unable to get rect for matched element'); + } + + /** + * Matching is done on a resized reference image to account for device pixel density. + * However, the coordinates returned by getImageOccurrence are relative to the resized buffer, + * *not* the original screen element. This leads to incorrect tap positions unless we + * scale the match result back down to the actual dimensions of the element. + * The logic below handles this scaling correction, ensuring the tap lands at the correct + * screen coordinates — even when Retina displays and image resizing are involved. + */ + const { matchRect, resizedMeta } = bestResult; + const scaleX = rect.width / (resizedMeta.width ?? rect.width); + const scaleY = rect.height / (resizedMeta.height ?? rect.height); + const matchCenterX = matchRect.x + Math.floor(matchRect.width / 2); + const matchCenterY = matchRect.y + Math.floor(matchRect.height / 2); + const tapX = Math.round(rect.x + matchCenterX * scaleX); + const tapY = Math.round(rect.y + matchCenterY * scaleY); + + await clickOnCoordinates(this, { x: tapX, y: tapY }); } + /** * Checks if an element exists on the screen without throwing an error. * Only useful for scenarios where you want to interact with an element if it exists @@ -1221,7 +1240,7 @@ export class DeviceWrapper { maxWait?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { locator, description } = this.resolveLocator(args); const maxWait = args.maxWait || 2_000; // Wait for any transitions to complete @@ -1229,8 +1248,6 @@ export class DeviceWrapper { const element = await this.findElementQuietly(locator, args.text); - const description = describeLocator({ ...locator, text: args.text }); - if (element) { // Elements can disappear in the GUI but still be present in the DOM let isVisible: boolean; @@ -1275,13 +1292,11 @@ export class DeviceWrapper { maxWait?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { locator, description } = this.resolveLocator(args); const text = args.text; const initialMaxWait = args.initialMaxWait ?? 10_000; const maxWait = args.maxWait ?? 30_000; - const description = describeLocator({ ...locator, text: args.text }); - // Track total time from start - disappearing timers begin on send, not on display const functionStartTime = Date.now(); // Phase 1: Wait for element to appear @@ -1326,13 +1341,11 @@ export class DeviceWrapper { maxWait?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { locator, description } = this.resolveLocator(args); const text = args.text; const initialMaxWait = args.initialMaxWait ?? 10_000; const maxWait = args.maxWait ?? 30_000; - const description = describeLocator({ ...locator, text: args.text }); - // Phase 1: Wait for element to appear this.log(`Waiting for element with ${description} to be deleted...`); await this.waitForElementToAppear(locator, initialMaxWait, text); @@ -1702,8 +1715,7 @@ export class DeviceWrapper { expectedColor: string, tolerance?: number ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; - const description = describeLocator({ ...locator, text: args.text }); + const { locator, description } = this.resolveLocator(args); this.log(`Waiting for ${description} to have color #${expectedColor}`); @@ -1788,14 +1800,6 @@ export class DeviceWrapper { await this.onAndroid().clickOnElementAll(new AcceptMessageRequestButton(this)); } - public async sendMessageTo(sender: User, receiver: Group | User) { - const message = `${sender.userName} to ${receiver.userName}`; - await this.clickOnElementAll(new ConversationItem(this, receiver.userName)); - this.log(`${sender.userName} + " sent message to ${receiver.userName}`); - await this.sendMessage(message); - this.log(`Message received by ${receiver.userName} from ${sender.userName}`); - return message; - } // TODO instead of blind sleeping, check presence of reply preview // Remove blind sleep from other tests that reply as well public async replyToMessage(user: Pick, body: string) { @@ -1830,24 +1834,48 @@ export class DeviceWrapper { public async inputText( textToInput: string, - args: LocatorsInterface | ({ maxWait?: number } & StrategyExtractionObj) + args: LocatorsInterface | ({ maxWait?: number } & StrategyExtractionObj), + paste: boolean = false ) { const locator = args instanceof LocatorsInterface ? args.build() : args; - - this.log('Locator being used:', locator); - const el = await this.waitForTextElementToBePresent({ ...locator }); - if (!el) { - throw new Error(`inputText: Did not find element with locator: ${JSON.stringify(locator)}`); - } - await this.setValueImmediate(textToInput, el.ELEMENT); + if (paste) { + // Set clipboard, press key-code for instant paste + await this.clickOnElementAll({ ...locator }); + if (this.isAndroid()) { + await this.toAndroid().setClipboard( + Buffer.from(textToInput).toString('base64'), + 'plaintext' + ); + await this.toAndroid().pressKeyCode(279); + } else { + // Use native paste UI, accept perms if needed + await this.toIOS().mobileSetPasteboard(textToInput); + await this.toIOS().mobileGetPasteboard(); + await this.processPermissions({ strategy: 'accessibility id', selector: 'Allow Paste' }); + await this.clickOnElementAll({ ...locator }); + await this.clickOnByAccessibilityID('Paste'); + } + } else { + await this.setValueImmediate(textToInput, el.ELEMENT); + } } public async getAttribute(attribute: string, elementId: string) { return this.toShared().getAttribute(attribute, elementId); } + public async assertAttribute( + element: LocatorsInterface | StrategyExtractionObj, + attribute: string, + value: string + ) { + const el = await this.waitForTextElementToBePresent(element); + const received = await this.getAttribute(attribute, el.ELEMENT); + expect(received, 'Element attribute value mismatch').toBe(value); + } + public async disappearRadioButtonSelected( platform: SupportedPlatformsType, timeOption: DISAPPEARING_TIMES @@ -1877,7 +1905,12 @@ export class DeviceWrapper { } public async pushMediaToDevice( - mediaFileName: 'profile_picture.jpg' | 'test_file.pdf' | 'test_image.jpg' | 'test_video.mp4' + mediaFileName: + | 'animated_profile_picture.gif' + | 'profile_picture.jpg' + | 'test_file.pdf' + | 'test_image.jpg' + | 'test_video.mp4' ) { const filePath = path.join('run', 'test', 'media', mediaFileName); await fs.access(filePath).catch(() => { @@ -1913,7 +1946,6 @@ export class DeviceWrapper { await this.clickOnElementAll(new ImagesFolderButton(this)); await sleepFor(1000); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); - await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, testImage @@ -1931,12 +1963,12 @@ export class DeviceWrapper { await sleepFor(500); await this.clickOnElementAll({ strategy: 'id', - selector: 'network.loki.messenger:id/mediapicker_folder_item_thumbnail', + selector: 'mediapicker-folder-item-thumbnail-0', }); await sleepFor(100); await this.clickOnElementAll({ strategy: 'id', - selector: 'network.loki.messenger:id/mediapicker_image_item_thumbnail', + selector: 'mediapicker-image-item-thumbnail-0', }); } await this.inputText(message, new MessageInput(this)); @@ -1960,7 +1992,6 @@ export class DeviceWrapper { strategy: 'accessibility id', selector: 'Allow Full Access', }); - await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise // A video can't be matched by its thumbnail so we use a video thumbnail file await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, @@ -2161,7 +2192,17 @@ export class DeviceWrapper { return sentTimestamp; } - public async uploadProfilePicture() { + public async uploadProfilePicture(animated: boolean = false) { + let uploadPicture: 'animated_profile_picture.gif' | 'profile_picture.jpg'; + let dpLocator: LocatorsInterface; + if (animated) { + uploadPicture = animatedProfilePicture; + dpLocator = new GIFName(this); + } else { + uploadPicture = profilePicture; + dpLocator = new ImageName(this); + } + await this.clickOnElementAll(new UserSettings(this)); // Click on Profile picture await this.clickOnElementAll(new UserAvatar(this)); @@ -2169,15 +2210,14 @@ export class DeviceWrapper { // iOS files are pre-loaded on simulator creation, no need to push if (this.isIOS()) { await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); - await sleepFor(5000); // sometimes Appium doesn't recognize the XPATH immediately await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, - profilePicture + uploadPicture ); await this.clickOnByAccessibilityID('Done'); } else if (this.isAndroid()) { // Push file first - await this.pushMediaToDevice(profilePicture); + await this.pushMediaToDevice(uploadPicture); await this.clickOnElementAll(new ImagePermissionsModalAllow(this)); await sleepFor(1000); await this.clickOnElementAll({ @@ -2185,8 +2225,10 @@ export class DeviceWrapper { selector: 'Image button', }); await sleepFor(500); - await this.clickOnElementAll(new ImageName(this)); - await this.clickOnElementById('network.loki.messenger:id/crop_image_menu_crop'); + await this.clickOnElementAll(dpLocator); + if (!animated) { + await this.clickOnElementById('network.loki.messenger:id/crop_image_menu_crop'); + } } await this.clickOnElementAll(new SaveProfilePictureButton(this)); } @@ -2369,19 +2411,15 @@ export class DeviceWrapper { public async turnOnReadReceipts() { await this.navigateBack(); - await sleepFor(100); await this.clickOnElementAll(new UserSettings(this)); - await sleepFor(500); await this.clickOnElementAll(new PrivacyMenuItem(this)); - await sleepFor(2000); await this.clickOnElementAll(new ReadReceiptsButton(this)); await this.navigateBack(false); - await sleepFor(100); await this.clickOnElementAll(new CloseSettings(this)); } - public async processPermissions(locator: LocatorsInterface) { - const locatorConfig = locator.build(); + public async processPermissions(locator: LocatorsInterface | StrategyExtractionObj) { + const locatorConfig = locator instanceof LocatorsInterface ? locator.build() : locator; if (this.isAndroid()) { const permissions = await this.doesElementExist({ @@ -2529,21 +2567,7 @@ export class DeviceWrapper { this.assertTextMatches(actualDescription, expectedDescription, 'Modal description'); } - /** - * Checks CTA component text against expected values. - * CTAs contain: heading, body, 0-3 features, 1-2 buttons. - * @param heading - Expected CTA heading text - * @param body - Expected CTA body text - * @param buttons - Expected button text(s). First is positive, second (if present) is negative - * @param features - Optional array of expected feature text (0-3 items) - * @throws Error if any text element doesn't match expected value - */ - public async checkCTAStrings( - heading: string, - body: string, - buttons: string[], - features?: string[] - ): Promise { + private async checkCTAStrings({ heading, body, buttons, features }: CTAConfig): Promise { // Validate input if (features && features.length > 3) { throw new Error('CTAs support maximum 3 features'); @@ -2552,41 +2576,68 @@ export class DeviceWrapper { throw new Error('CTAs must have 1-2 buttons'); } - // Find and check heading + // CTA heading const elHeading = await this.waitForTextElementToBePresent(new CTAHeading(this)); const actualHeading = await this.getTextFromElement(elHeading); this.assertTextMatches(actualHeading, heading, 'CTA heading'); - // Find and check body + // CTA body const elBody = await this.waitForTextElementToBePresent(new CTABody(this)); const actualBody = await this.getTextFromElement(elBody); this.assertTextMatches(actualBody, body, 'CTA body'); - // Check features if expected + // CTA features if present if (features) { for (let i = 0; i < features.length; i++) { - const featureLocator = new CTAFeature(this, i); - const elFeature = await this.waitForTextElementToBePresent(featureLocator); + const elFeature = await this.waitForTextElementToBePresent(new CTAFeature(this, i)); const actualFeature = await this.getTextFromElement(elFeature); - this.assertTextMatches(actualFeature, features[i], `CTA feature ${i}`); + this.assertTextMatches(actualFeature, features[i], `CTA feature ${i + 1}`); } } - // Check buttons - const positiveLocator = new CTAButtonPositive(this); - const elPositive = await this.waitForTextElementToBePresent(positiveLocator); - const actualPositive = await this.getTextFromElement(elPositive); - this.assertTextMatches(actualPositive, buttons[0], 'CTA positive button'); + /** + * buttons[0] = negative/dismiss (always present); + * buttons[1] = positive/action (optional) + */ + const elNegative = await this.waitForTextElementToBePresent(new CTAButtonNegative(this)); + const actualNegative = await this.getTextFromElement(elNegative); + this.assertTextMatches(actualNegative, buttons[0], 'CTA negative button'); if (buttons.length === 2) { - const negativeLocator = new CTAButtonNegative(this); - const elNegative = await this.waitForTextElementToBePresent(negativeLocator); - const actualNegative = await this.getTextFromElement(elNegative); - this.assertTextMatches(actualNegative, buttons[1], 'CTA negative button'); + const elPositive = await this.waitForTextElementToBePresent(new CTAButtonPositive(this)); + const actualPositive = await this.getTextFromElement(elPositive); + this.assertTextMatches(actualPositive, buttons[1], 'CTA positive button'); + } + } + + public async checkCTA(type: CTAType): Promise { + await this.checkCTAStrings(ctaConfigs[type]); + } + + // This is the bare minimum of a CTA so we only check these + public async verifyNoCTAShows(): Promise { + await Promise.all([ + this.verifyElementNotPresent(new CTAHeading(this)), + this.verifyElementNotPresent(new CTABody(this)), + this.verifyElementNotPresent(new CTAButtonNegative(this)), + ]); + } + + // Dismiss any CTA if it shows + public async dismissCTA(): Promise { + const hasCTAAppeared = await this.doesElementExist({ + ...new CTAButtonNegative(this).build(), + maxWait: 8_000, + }); + if (hasCTAAppeared) { + this.log('Dismissing CTA'); + await this.clickOnElementAll(new CTAButtonNegative(this)); } } - public async getElementPixelColor(args: LocatorsInterface): Promise { + public async getElementPixelColor( + args: LocatorsInterface | StrategyExtractionObj + ): Promise { // Wait for the element to be present const element = await this.waitForTextElementToBePresent(args); // Take a screenshot and return a hex color value @@ -2595,6 +2646,24 @@ export class DeviceWrapper { return pixelColor; } + // Sample an element's centre pixel color SAMPLE_SIZE times to determine whether it is animated or not. + // If the set contains more than 1 color it is likely animated. + public async verifyElementIsAnimated( + args: LocatorsInterface | StrategyExtractionObj + ): Promise { + const { locator, description } = this.resolveLocator(args); + this.log(`Checking if ${description} is animated`); + const SAMPLE_SIZE = 3; + const colors = new Set(); + for (let i = 0; i < SAMPLE_SIZE; i++) { + colors.add(await this.getElementPixelColor(locator)); + } + expect( + colors.size, + `Expected element to be animated but detected 1 unique color: ${[...colors][0]}` + ).toBeGreaterThan(1); + } + public async getVersionNumber() { // NOTE if this becomes necessary for more tests, consider adding a property/caching to the DeviceWrapper await this.clickOnElementAll(new UserSettings(this)); diff --git a/run/types/allure.ts b/run/types/allure.ts index a94054f41a..5872894b71 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -31,7 +31,11 @@ export type AllureSuiteConfig = parent: 'Sending Messages'; suite: 'Emoji reacts' | 'Mentions' | 'Message types' | 'Performance' | 'Rules'; } - | { parent: 'Settings'; suite: 'App Disguise' | 'Community Message Requests' | 'Notifications' } + | { parent: 'Session Pro' } + | { + parent: 'Settings'; + suite: 'App Disguise' | 'Community Message Requests' | 'Notifications' | 'Recovery Password'; + } | { parent: 'User Actions'; suite: diff --git a/run/types/cta.ts b/run/types/cta.ts new file mode 100644 index 0000000000..6e0aa59b1a --- /dev/null +++ b/run/types/cta.ts @@ -0,0 +1,47 @@ +import { tStripped } from '../localizer/lib'; + +export type CTAType = 'alreadyActivated' | 'animatedProfilePicture' | 'donate' | 'longerMessages'; + +/** + * buttons[0] is the negative/dismiss button (always present); + * buttons[1] is the optional positive/action button + */ +export type CTAConfig = { + heading: string; + body: string; + buttons: [string, string] | [string]; + features?: string[]; +}; + +export const ctaConfigs: Record = { + donate: { + heading: tStripped('donateSessionHelp'), + body: tStripped('donateSessionDescription'), + buttons: [tStripped('maybeLater'), tStripped('donate')], + }, + longerMessages: { + heading: tStripped('upgradeTo'), + body: tStripped('proCallToActionLongerMessages'), + buttons: [tStripped('cancel'), tStripped('theContinue')], + features: [ + tStripped('proFeatureListLongerMessages'), + tStripped('proFeatureListPinnedConversations'), + tStripped('proFeatureListLoadsMore'), + ], + }, + animatedProfilePicture: { + heading: tStripped('upgradeTo'), + body: tStripped('proAnimatedDisplayPictureCallToActionDescription'), + buttons: [tStripped('cancel'), tStripped('theContinue')], + features: [ + tStripped('proFeatureListAnimatedDisplayPicture'), + tStripped('proFeatureListLongerMessages'), + tStripped('proFeatureListLoadsMore'), + ], + }, + alreadyActivated: { + heading: tStripped('proActivated'), + body: tStripped('proAnimatedDisplayPicture'), + buttons: [tStripped('close')], + }, +}; diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index 0b1be85bbd..e329b16be1 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -5,12 +5,10 @@ import { omit } from 'lodash'; import type { AppCountPerTest } from '../test/state_builder'; import { setupAllureTestInfo } from '../test/utils/allure/allureHelpers'; +import { unregisterDevicesForTest } from '../test/utils/device_registry'; import { getNetworkTarget } from '../test/utils/devnet'; +import { captureLogsOnFailure, captureScreenshotsOnFailure } from '../test/utils/failure_artifacts'; import { SupportedPlatformsType } from '../test/utils/open_app'; -import { - captureScreenshotsOnFailure, - unregisterDevicesForTest, -} from '../test/utils/screenshot_helper'; import { AllureSuiteConfig } from './allure'; import { TestRisk } from './testing'; @@ -105,9 +103,10 @@ function mobileIt({ testInfo.status === 'timedOut' ) { await captureScreenshotsOnFailure(testInfo); + await captureLogsOnFailure(testInfo); } - } catch (screenshotError) { - console.error('Failed to capture screenshot:', screenshotError); + } catch (artifactError) { + console.error('Failed to capture failure artifacts:', artifactError); } try { diff --git a/run/types/testing.ts b/run/types/testing.ts index e8032a0180..5c6710d6e3 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -124,6 +124,7 @@ export type XPath = | `(//XCUIElementTypeImage[@name="gif cell"])[1]` | `//*[./*[@name='${DISAPPEARING_TIMES}']]/*[2]` | `//*[@resource-id='network.loki.messenger:id/callTitle' and contains(@text, ':')]` + | `//*[starts-with(@content-desc, "GIF taken on")]` | `//*[starts-with(@content-desc, "Photo taken on")]` | `//android.view.ViewGroup[@resource-id='network.loki.messenger:id/mainContainer'][.//android.widget.TextView[contains(@text,'${string}')]]//androidx.compose.ui.platform.ComposeView[@resource-id='network.loki.messenger:id/profilePictureView']` | `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger:id/layout_emoji_container"]` @@ -182,6 +183,7 @@ export type AccessibilityId = | 'Allow' | 'Allow Access to All Photos' | 'Allow Full Access' + | 'Allow Paste' | 'Allow voice and video calls' | 'All Photos' | 'Answer call' @@ -233,6 +235,10 @@ export type AccessibilityId = | 'Copy URL' | 'Create account button' | 'Create group' + | 'cta-body' + | 'cta-button-negative' + | 'cta-button-positive' + | 'cta-heading' | 'Decline message request' | 'Delete' | 'Delete Contact' @@ -351,13 +357,13 @@ export type AccessibilityId = | 'open-survey-button' | 'Open' | 'Open URL' + | 'Paste' | 'Path' | 'Photo library' | 'Photos' | 'Pin' | 'Please enter a shorter group name' | 'Privacy Policy' - | 'qa-blocked-contacts-settings-item' | 'rate-app-button' | 'Read Receipts - Switch' | 'Recents' @@ -424,6 +430,7 @@ export type AccessibilityId = | 'Your message request has been accepted.' | `${DISAPPEARING_TIMES} - Radio` | `${GROUPNAME}` + | `cta-feature-${number}` | `Disappear after ${DisappearActions} option`; export type Id = @@ -435,7 +442,6 @@ export type Id = | 'android:id/alertTitle' | 'android:id/button1' | 'android:id/content_preview_text' - | 'android:id/summary' | 'android:id/title' | 'android.widget.TextView' | 'Appearance' @@ -522,6 +528,8 @@ export type Id = | 'manage-admins-menu-option' | 'manage-members-menu-option' | 'Market cap amount' + | 'mediapicker-folder-item-thumbnail-0' + | 'mediapicker-image-item-thumbnail-0' | 'MeetingSE option' | 'Modal description' | 'Modal heading' @@ -540,8 +548,6 @@ export type Id = | 'network.loki.messenger:id/endCallButton' | 'network.loki.messenger:id/layout_emoji_container' | 'network.loki.messenger:id/linkPreviewView' - | 'network.loki.messenger:id/mediapicker_folder_item_thumbnail' - | 'network.loki.messenger:id/mediapicker_image_item_thumbnail' | 'network.loki.messenger:id/messageStatusTextView' | 'network.loki.messenger:id/openGroupTitleTextView' | 'network.loki.messenger:id/play_overlay' @@ -566,6 +572,10 @@ export type Id = | 'open-survey-button' | 'Open' | 'Open URL' + | 'preferences-dialog-option-enable' + | 'preferences-option-blocked-contacts' + | 'preferences-option-read-receipt' + | 'preferences-option-whitelist-toggle' | 'preferred-display-name' | 'Privacy' | 'Privacy policy button' @@ -624,6 +634,7 @@ export type ScreenshotFileNames = | 'conversation_alice' | 'conversation_bob' | 'cta_donate' + | 'cta_pro_activated' | 'landingpage_new_account' | 'landingpage_restore_account' | 'settings_appearance' diff --git a/run/types/tuple.d.ts b/run/types/tuple.d.ts deleted file mode 100644 index 8455b68740..0000000000 --- a/run/types/tuple.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -type TupleOf> = R['length'] extends N - ? R - : TupleOf; - -export type Tuple = N extends N - ? number extends N - ? Array - : TupleOf - : never; diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index 79a008534d..051616aac8 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -44,7 +44,7 @@ const DEVICE_CONFIG = { const MEDIA_ROOT = path.join('run', 'test', 'media'); const MEDIA_FILES = { - images: ['profile_picture.jpg', 'test_image.jpg'], + images: ['profile_picture.jpg', 'test_image.jpg', 'animated_profile_picture.gif'], videos: ['test_video.mp4'], pdfs: ['test_file.pdf'], };