From 14d69d23c1a371a14fc8c133538a854d949c5892 Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Fri, 21 Aug 2026 15:53:35 -0300 Subject: [PATCH 1/4] fix(id-docs): keep ldap documents visible and complete approval LDAP accounts are missing from oc_users, so an inner join hid their identification documents. The uploader placeholder sign request also blocked approval from reaching SIGNED. Fixes #7861 Fixes #7860 Signed-off-by: Luis Amorim --- lib/Db/IdDocsMapper.php | 3 +- lib/Service/SignFileService.php | 37 +++++++++- tests/php/Unit/Db/IdDocsMapperTest.php | 70 +++++++++++++++++++ .../php/Unit/Service/SignFileServiceTest.php | 53 ++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 tests/php/Unit/Db/IdDocsMapperTest.php diff --git a/lib/Db/IdDocsMapper.php b/lib/Db/IdDocsMapper.php index 4805d23a10..b22f9a703b 100644 --- a/lib/Db/IdDocsMapper.php +++ b/lib/Db/IdDocsMapper.php @@ -192,8 +192,7 @@ private function getQueryBuilder(array $filter = [], bool $count = false): IQuer ->leftJoin('id', 'libresign_sign_request', 'sr', 'id.sign_request_id = sr.id'); if ($needsUserJoin) { - $joinType = !empty($filter['userId']) ? 'join' : 'leftJoin'; - $qb->$joinType('id', 'users', 'u', 'id.user_id = u.uid'); + $qb->leftJoin('id', 'users', 'u', 'id.user_id = u.uid'); } if (!empty($filter['userId'])) { diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 422a7e30ae..fe6eda72da 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -1045,7 +1045,7 @@ private function updateEntityCacheAfterDbSave(FileEntity $file): void { } private function evaluateStatusFromSigners(): ?int { - $signers = $this->getSigners(); + $signers = $this->excludeIdDocUploaderPlaceholder($this->getSigners()); $total = count($signers); @@ -1066,6 +1066,41 @@ private function evaluateStatusFromSigners(): ?int { return null; } + /** + * @param SignRequestEntity[] $signers + * @return SignRequestEntity[] + */ + private function excludeIdDocUploaderPlaceholder(array $signers): array { + $placeholderId = $this->getIdDocUploaderSignRequestId(); + if ($placeholderId === null) { + return $signers; + } + + return array_values(array_filter( + $signers, + static fn (SignRequestEntity $signer): bool => $signer->getId() !== $placeholderId, + )); + } + + private function getIdDocUploaderSignRequestId(): ?int { + $fileId = $this->signRequest->getFileId(); + if ($fileId === null) { + return null; + } + + try { + $idDocs = $this->idDocsMapper->getByFileId($fileId); + } catch (\Throwable) { + return null; + } + + if (!$idDocs instanceof IdDocs) { + return null; + } + + return $idDocs->getSignRequestId(); + } + private function getOrGeneratePfxContent(SignEngineHandler $engine): string { $result = $this->pfxProvider->getOrGeneratePfx( $engine, diff --git a/tests/php/Unit/Db/IdDocsMapperTest.php b/tests/php/Unit/Db/IdDocsMapperTest.php new file mode 100644 index 0000000000..d1c4065adb --- /dev/null +++ b/tests/php/Unit/Db/IdDocsMapperTest.php @@ -0,0 +1,70 @@ +fileMapper = Server::get(FileMapper::class); + $this->idDocsMapper = Server::get(IdDocsMapper::class); + $this->signRequestMapper = Server::get(SignRequestMapper::class); + } + + public function testListByUserIdReturnsDocumentsWhenAccountIsMissingFromUsersTable(): void { + $ldapUserId = 'ldap-user-without-oc-users-row'; + + $file = new File(); + $file->setNodeId(80808); + $file->setUserId($ldapUserId); + $file->setUuid('c3333333-3333-4333-8333-333333333333'); + $file->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC'))); + $file->setName('passport.pdf'); + $file->setStatus(FileStatus::ABLE_TO_SIGN->value); + $insertedFile = $this->fileMapper->insert($file); + + $signRequest = new SignRequest(); + $signRequest->setFileId($insertedFile->getId()); + $signRequest->setDisplayName('LDAP User'); + $signRequest->setUuid('d4444444-4444-4444-8444-444444444444'); + $signRequest->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC'))); + $insertedSignRequest = $this->signRequestMapper->insert($signRequest); + + $this->idDocsMapper->save( + $insertedFile->getId(), + $insertedSignRequest->getId(), + $ldapUserId, + 'IDENTIFICATION', + ); + + $result = $this->idDocsMapper->list( + ['userId' => $ldapUserId], + page: 1, + length: 10, + ); + + $this->assertCount(1, $result['data']); + $this->assertSame($insertedFile->getUuid(), $result['data'][0]['file']['uuid']); + $this->assertSame('LDAP User', $result['data'][0]['account']['displayName']); + } +} diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index 4b151b353a..ee4b8bcb92 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -29,6 +29,7 @@ use OCA\Libresign\Enum\DocMdpLevel; use OCA\Libresign\Enum\FileStatus; use OCA\Libresign\Enum\FileStatus as FileStatusEnum; +use OCA\Libresign\Enum\SignRequestStatus; use OCA\Libresign\Events\SignedEvent; use OCA\Libresign\Events\SignedEventFactory; use OCA\Libresign\Exception\LibresignException; @@ -986,6 +987,58 @@ private static function generateSigners(int $total, int $signed): array { return $signers; } + public function testIdDocApprovalReachesSignedWhenUploaderPlaceholderIsUnsigned(): void { + $uploader = new SignRequest(); + $uploader->setId(10); + $uploader->setStatus(SignRequestStatus::DRAFT->value); + + $approver = new SignRequest(); + $approver->setId(20); + $approver->setSigned(new DateTime()); + $approver->setStatus(SignRequestStatus::SIGNED->value); + + $idDocs = new IdDocs(); + $idDocs->setSignRequestId(10); + $this->idDocsMapper + ->method('getByFileId') + ->with(1) + ->willReturn($idDocs); + + $service = $this->getService(['getSigners']); + $service->method('getSigners')->willReturn([$uploader, $approver]); + + $signRequest = new SignRequest(); + $signRequest->setFileId(1); + $service->setSignRequest($signRequest); + + $status = self::invokePrivate($service, 'evaluateStatusFromSigners'); + $this->assertSame(FileStatus::SIGNED->value, $status); + } + + public function testDraftSignersStillCountWhenFileIsNotAnIdentificationDocument(): void { + $this->idDocsMapper + ->method('getByFileId') + ->willThrowException(new DoesNotExistException('no identification document')); + + $signed = new SignRequest(); + $signed->setId(1); + $signed->setSigned(new DateTime()); + + $draft = new SignRequest(); + $draft->setId(2); + $draft->setStatus(SignRequestStatus::DRAFT->value); + + $service = $this->getService(['getSigners']); + $service->method('getSigners')->willReturn([$signed, $draft]); + + $signRequest = new SignRequest(); + $signRequest->setFileId(99); + $service->setSignRequest($signRequest); + + $status = self::invokePrivate($service, 'evaluateStatusFromSigners'); + $this->assertSame(FileStatus::PARTIAL_SIGNED->value, $status); + } + #[DataProvider('providerGetEngineWillWorkWithLazyLoadedEngine')] public function testGetEngineWillWorkWithLazyLoadedEngine(string $extension, string $instanceOf): void { $expectedEngine = $this->createMock($instanceOf); From 78737bbc448547936f72e8ca316a0006d0cc1f9d Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Wed, 19 Aug 2026 15:34:40 -0300 Subject: [PATCH 2/4] test(id-docs): cover identification document upload in playwright Signed-off-by: Luis Amorim --- playwright/e2e/id-docs-visual.spec.ts | 122 ++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 playwright/e2e/id-docs-visual.spec.ts diff --git a/playwright/e2e/id-docs-visual.spec.ts b/playwright/e2e/id-docs-visual.spec.ts new file mode 100644 index 0000000000..9405ac1bf0 --- /dev/null +++ b/playwright/e2e/id-docs-visual.spec.ts @@ -0,0 +1,122 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test, type Locator, type Page } from '@playwright/test' +import { mkdir } from 'node:fs/promises' +import { resolve } from 'node:path' + +import { login } from '../support/nc-login' + +const SCREENSHOT_DIR = resolve(process.cwd(), 'playwright/.visual-output') + +test.describe.configure({ mode: 'serial', timeout: 180000 }) + +async function shot(page: Page, name: string): Promise { + await mkdir(SCREENSHOT_DIR, { recursive: true }) + const path = resolve(SCREENSHOT_DIR, `${name}.png`) + await page.screenshot({ path, fullPage: true }) + return path +} + +function emptyStatus(page: Page): Locator { + return page.getByText(/Not sent yet|Ainda não enviado/i) +} + +function deleteButton(page: Page): Locator { + return page.getByRole('button', { name: /Delete file|Excluir arquivo/i }) +} + +function uploadButton(page: Page): Locator { + return page.getByRole('button', { name: /Upload file|Enviar arquivo/i }) +} + +async function waitForIdDocsCard(page: Page): Promise { + await expect(emptyStatus(page).or(deleteButton(page))).toBeVisible({ timeout: 20_000 }) +} + +async function clearExistingIdDocument(page: Page): Promise { + await waitForIdDocsCard(page) + for (let attempt = 0; attempt < 5; attempt++) { + if (!(await deleteButton(page).isVisible().catch(() => false))) { + break + } + await deleteButton(page).click() + await expect(page.getByText(/File was deleted\.|Arquivo foi apagado\./i)).toBeVisible({ timeout: 20_000 }) + await waitForIdDocsCard(page) + } + await expect(emptyStatus(page)).toBeVisible({ timeout: 20_000 }) + await expect(uploadButton(page)).toBeVisible() +} + +test('identification documents appear on the account page after upload', async ({ page }) => { + test.slow() + + await login( + page.request, + process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', + process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', + ) + + const policyResponse = await page.request.post( + './ocs/v2.php/apps/libresign/api/v1/policies/system/identification_documents?format=json', + { + headers: { + 'OCS-ApiRequest': 'true', + Accept: 'application/json', + Authorization: 'Basic ' + Buffer.from('admin:admin').toString('base64'), + 'Content-Type': 'application/json', + }, + data: { + value: { enabled: true, approvers: ['admin'] }, + }, + }, + ) + expect(policyResponse.ok(), await policyResponse.text()).toBeTruthy() + + await page.goto('./apps/libresign') + await expect(page.getByRole('button', { name: /Upload from URL|Carregar do URL/i })).toBeVisible({ timeout: 20_000 }) + await expect(page.getByText(/Document Validation|Validação de Documentos/i).first()).toBeVisible() + await shot(page, '01-libresign-home') + + await page.goto('./apps/libresign/f/account') + await expect(page.getByRole('heading', { name: /Identification documents|Documentos de identificação/i })).toBeVisible({ timeout: 20_000 }) + await clearExistingIdDocument(page) + await shot(page, '02-account-id-docs-empty') + + const [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser'), + uploadButton(page).click(), + ]) + await fileChooser.setFiles(resolve(process.cwd(), 'tests/php/fixtures/pdfs/small_valid.pdf')) + + await expect(page.getByText(/File was sent\.|Arquivo foi enviado\./i)).toBeVisible({ timeout: 20_000 }) + await expect(deleteButton(page)).toBeVisible({ timeout: 20_000 }) + await expect(emptyStatus(page)).toHaveCount(0) + await shot(page, '03-account-id-doc-uploaded') + + await page.goto('./apps/libresign/f/docs/id-docs/validation') + await expect(page.getByRole('columnheader', { name: /Owner|Proprietário/i })).toBeVisible({ timeout: 20_000 }) + await expect(page.getByRole('cell', { name: /^admin$/i }).first()).toBeVisible({ timeout: 20_000 }) + await expect(page.getByText(/waiting for approval|aguardando aprovação/i).first()).toBeVisible() + await expect(page.getByRole('button', { name: /Sign|Assinar/i }).first()).toBeVisible() + await shot(page, '04-id-docs-approval-list') + + await page.goto('./settings/admin/libresign') + const catalogSearch = page.locator('.policy-workbench__catalog-search').getByRole('textbox').first() + await expect(catalogSearch).toBeVisible({ timeout: 20_000 }) + + const collapseButton = page.getByRole('button', { + name: /Collapse settings categories|Recolher categorias de configurações|Expand settings categories|Expandir categorias de configurações/i, + }).first() + if (/Expand|Expandir/i.test((await collapseButton.getAttribute('aria-label')) ?? '')) { + await collapseButton.click() + } + + await catalogSearch.fill('identifica') + await expect(page.getByRole('button', { + name: /Identification documents flow|Fluxo de documentos de identificação/i, + }).first()).toBeVisible({ timeout: 20_000 }) + await shot(page, '05-settings-identification-documents') +}) From 0b1cf62bd03d159c04cc77fd8da929f40b67c00d Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Wed, 19 Aug 2026 16:24:41 -0300 Subject: [PATCH 3/4] test(id-docs): isolate identification documents playwright spec Restore the system policy after the spec and match the English Documents Validation sidebar label so later Playwright tests are not affected. Signed-off-by: Luis Amorim --- .gitignore | 1 + playwright/e2e/id-docs-visual.spec.ts | 68 ++++++++++++++++++--------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 4ee4c89951..56e6844c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ node_modules/ /coverage /dist/ /test-results/ +/playwright/.visual-output/ diff --git a/playwright/e2e/id-docs-visual.spec.ts b/playwright/e2e/id-docs-visual.spec.ts index 9405ac1bf0..c9b1e5ec0f 100644 --- a/playwright/e2e/id-docs-visual.spec.ts +++ b/playwright/e2e/id-docs-visual.spec.ts @@ -3,21 +3,59 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { expect, test, type Locator, type Page } from '@playwright/test' import { mkdir } from 'node:fs/promises' import { resolve } from 'node:path' -import { login } from '../support/nc-login' +import { expect, test, type APIRequestContext, type Locator, type Page } from '@playwright/test' +import { login } from '../support/nc-login' +import { + createAuthenticatedRequestContext, + getSystemPolicySnapshot, + policyRequest, + restoreSystemPolicySnapshot, + type SystemPolicySnapshot, +} from '../support/policy-api' + +const POLICY_KEY = 'identification_documents' const SCREENSHOT_DIR = resolve(process.cwd(), 'playwright/.visual-output') test.describe.configure({ mode: 'serial', timeout: 180000 }) -async function shot(page: Page, name: string): Promise { +let adminContext: APIRequestContext +let originalPolicy: SystemPolicySnapshot + +test.beforeEach(async () => { + const adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin' + const adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin' + adminContext = await createAuthenticatedRequestContext(adminUser, adminPassword) + originalPolicy = await getSystemPolicySnapshot(adminContext, POLICY_KEY) + + const response = await policyRequest( + adminContext, + 'POST', + `/apps/libresign/api/v1/policies/system/${POLICY_KEY}`, + { + value: { enabled: true, approvers: [adminUser] }, + allowChildOverride: true, + }, + ) + expect(response.httpStatus, response.message).toBe(200) +}) + +test.afterEach(async () => { + try { + if (adminContext && originalPolicy) { + await restoreSystemPolicySnapshot(adminContext, POLICY_KEY, originalPolicy) + } + } finally { + await adminContext?.dispose() + } +}) + +async function shot(page: Page, name: string): Promise { await mkdir(SCREENSHOT_DIR, { recursive: true }) - const path = resolve(SCREENSHOT_DIR, `${name}.png`) - await page.screenshot({ path, fullPage: true }) - return path + await page.screenshot({ path: resolve(SCREENSHOT_DIR, `${name}.png`), fullPage: true }) } function emptyStatus(page: Page): Locator { @@ -59,25 +97,9 @@ test('identification documents appear on the account page after upload', async ( process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', ) - const policyResponse = await page.request.post( - './ocs/v2.php/apps/libresign/api/v1/policies/system/identification_documents?format=json', - { - headers: { - 'OCS-ApiRequest': 'true', - Accept: 'application/json', - Authorization: 'Basic ' + Buffer.from('admin:admin').toString('base64'), - 'Content-Type': 'application/json', - }, - data: { - value: { enabled: true, approvers: ['admin'] }, - }, - }, - ) - expect(policyResponse.ok(), await policyResponse.text()).toBeTruthy() - await page.goto('./apps/libresign') await expect(page.getByRole('button', { name: /Upload from URL|Carregar do URL/i })).toBeVisible({ timeout: 20_000 }) - await expect(page.getByText(/Document Validation|Validação de Documentos/i).first()).toBeVisible() + await expect(page.getByRole('link', { name: /Documents Validation|Validação de Documentos/i })).toBeVisible({ timeout: 20_000 }) await shot(page, '01-libresign-home') await page.goto('./apps/libresign/f/account') From 4c656b304c50b356f18bcdc0a5cf76af2d107c6d Mon Sep 17 00:00:00 2001 From: Luis Amorim Date: Fri, 21 Aug 2026 18:44:31 -0300 Subject: [PATCH 4/4] fix(id-docs): address PR review on status lookup and playwright Catch only DoesNotExistException for missing id docs, store visual screenshots under build/, and keep e2e selectors English-only. Signed-off-by: Luis Amorim Co-authored-by: Cursor --- .gitignore | 1 - lib/Service/SignFileService.php | 2 +- playwright/e2e/id-docs-visual.spec.ts | 30 +++++++++++++-------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 56e6844c4b..4ee4c89951 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,3 @@ node_modules/ /coverage /dist/ /test-results/ -/playwright/.visual-output/ diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index fe6eda72da..7263ac6294 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -1090,7 +1090,7 @@ private function getIdDocUploaderSignRequestId(): ?int { try { $idDocs = $this->idDocsMapper->getByFileId($fileId); - } catch (\Throwable) { + } catch (DoesNotExistException) { return null; } diff --git a/playwright/e2e/id-docs-visual.spec.ts b/playwright/e2e/id-docs-visual.spec.ts index c9b1e5ec0f..bcea9508b1 100644 --- a/playwright/e2e/id-docs-visual.spec.ts +++ b/playwright/e2e/id-docs-visual.spec.ts @@ -18,7 +18,7 @@ import { } from '../support/policy-api' const POLICY_KEY = 'identification_documents' -const SCREENSHOT_DIR = resolve(process.cwd(), 'playwright/.visual-output') +const SCREENSHOT_DIR = resolve(process.cwd(), 'build/playwright/visual-output') test.describe.configure({ mode: 'serial', timeout: 180000 }) @@ -59,15 +59,15 @@ async function shot(page: Page, name: string): Promise { } function emptyStatus(page: Page): Locator { - return page.getByText(/Not sent yet|Ainda não enviado/i) + return page.getByText(/Not sent yet/i) } function deleteButton(page: Page): Locator { - return page.getByRole('button', { name: /Delete file|Excluir arquivo/i }) + return page.getByRole('button', { name: /Delete file/i }) } function uploadButton(page: Page): Locator { - return page.getByRole('button', { name: /Upload file|Enviar arquivo/i }) + return page.getByRole('button', { name: /Upload file/i }) } async function waitForIdDocsCard(page: Page): Promise { @@ -81,7 +81,7 @@ async function clearExistingIdDocument(page: Page): Promise { break } await deleteButton(page).click() - await expect(page.getByText(/File was deleted\.|Arquivo foi apagado\./i)).toBeVisible({ timeout: 20_000 }) + await expect(page.getByText(/File was deleted\./i)).toBeVisible({ timeout: 20_000 }) await waitForIdDocsCard(page) } await expect(emptyStatus(page)).toBeVisible({ timeout: 20_000 }) @@ -98,12 +98,12 @@ test('identification documents appear on the account page after upload', async ( ) await page.goto('./apps/libresign') - await expect(page.getByRole('button', { name: /Upload from URL|Carregar do URL/i })).toBeVisible({ timeout: 20_000 }) - await expect(page.getByRole('link', { name: /Documents Validation|Validação de Documentos/i })).toBeVisible({ timeout: 20_000 }) + await expect(page.getByRole('button', { name: /Upload from URL/i })).toBeVisible({ timeout: 20_000 }) + await expect(page.getByRole('link', { name: /Documents Validation/i })).toBeVisible({ timeout: 20_000 }) await shot(page, '01-libresign-home') await page.goto('./apps/libresign/f/account') - await expect(page.getByRole('heading', { name: /Identification documents|Documentos de identificação/i })).toBeVisible({ timeout: 20_000 }) + await expect(page.getByRole('heading', { name: /Identification documents/i })).toBeVisible({ timeout: 20_000 }) await clearExistingIdDocument(page) await shot(page, '02-account-id-docs-empty') @@ -113,16 +113,16 @@ test('identification documents appear on the account page after upload', async ( ]) await fileChooser.setFiles(resolve(process.cwd(), 'tests/php/fixtures/pdfs/small_valid.pdf')) - await expect(page.getByText(/File was sent\.|Arquivo foi enviado\./i)).toBeVisible({ timeout: 20_000 }) + await expect(page.getByText(/File was sent\./i)).toBeVisible({ timeout: 20_000 }) await expect(deleteButton(page)).toBeVisible({ timeout: 20_000 }) await expect(emptyStatus(page)).toHaveCount(0) await shot(page, '03-account-id-doc-uploaded') await page.goto('./apps/libresign/f/docs/id-docs/validation') - await expect(page.getByRole('columnheader', { name: /Owner|Proprietário/i })).toBeVisible({ timeout: 20_000 }) + await expect(page.getByRole('columnheader', { name: /Owner/i })).toBeVisible({ timeout: 20_000 }) await expect(page.getByRole('cell', { name: /^admin$/i }).first()).toBeVisible({ timeout: 20_000 }) - await expect(page.getByText(/waiting for approval|aguardando aprovação/i).first()).toBeVisible() - await expect(page.getByRole('button', { name: /Sign|Assinar/i }).first()).toBeVisible() + await expect(page.getByText(/waiting for approval/i).first()).toBeVisible() + await expect(page.getByRole('button', { name: /Sign/i }).first()).toBeVisible() await shot(page, '04-id-docs-approval-list') await page.goto('./settings/admin/libresign') @@ -130,15 +130,15 @@ test('identification documents appear on the account page after upload', async ( await expect(catalogSearch).toBeVisible({ timeout: 20_000 }) const collapseButton = page.getByRole('button', { - name: /Collapse settings categories|Recolher categorias de configurações|Expand settings categories|Expandir categorias de configurações/i, + name: /Collapse settings categories|Expand settings categories/i, }).first() - if (/Expand|Expandir/i.test((await collapseButton.getAttribute('aria-label')) ?? '')) { + if (/Expand/i.test((await collapseButton.getAttribute('aria-label')) ?? '')) { await collapseButton.click() } await catalogSearch.fill('identifica') await expect(page.getByRole('button', { - name: /Identification documents flow|Fluxo de documentos de identificação/i, + name: /Identification documents flow/i, }).first()).toBeVisible({ timeout: 20_000 }) await shot(page, '05-settings-identification-documents') })