Skip to content

Commit 2a60596

Browse files
mklos-kwLukasHirt
authored andcommitted
tests: reduce OCM tests flakiness
1 parent 0f54c52 commit 2a60596

6 files changed

Lines changed: 150 additions & 10 deletions

File tree

.github/workflows/test.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ jobs:
181181
KEYCLOAK_HOST: ${{ matrix.suites.keycloak && 'localhost:8443' || '' }}
182182
run: cd ${{ github.workspace }}/tests/e2e && bash run-e2e.sh --type playwright
183183

184+
- name: Dump oCIS logs on failure
185+
if: failure()
186+
run: |
187+
echo "=== oCIS LOCAL: relevant log lines ==="
188+
grep -i "ocm\|provider\|invite\|federation\|error\|ERR\|permissions\|drives.*items\|Federated" /tmp/ocis-ocis.log 2>/dev/null | tail -300 || echo "(no log file)"
189+
echo "=== oCIS FEDERATED: relevant log lines ==="
190+
grep -i "ocm\|provider\|invite\|federation\|error\|ERR\|permissions\|drives.*items\|Federated" /tmp/ocis-ocis-federated.log 2>/dev/null | tail -300 || echo "(no log file)"
191+
184192
- name: Upload tracing result
185193
if: failure()
186194
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
@@ -196,3 +204,14 @@ jobs:
196204
name: playwright-a11y-result-${{ strategy.job-index }}-${{ github.run_attempt }}
197205
path: ${{ github.workspace }}/reports/e2e/a11y-report.json
198206
retention-days: 7
207+
208+
check-tests-passed:
209+
needs: [e2e-playwright]
210+
runs-on: ubuntu-latest
211+
if: always()
212+
steps:
213+
- name: Check all test jobs passed
214+
run: |
215+
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
216+
exit 1
217+
fi

tests/actions/setup-services.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,31 @@ fi
337337

338338
if $FEDERATION_ENABLED; then
339339
setup_ocis "ocis-federated" 10200
340+
wait_for_service "https://localhost:10200/.well-known/openid-configuration" "ocis-federated"
341+
342+
echo "=== Diagnostic: Graph role definitions on LOCAL (:9200) ==="
343+
curl -kfsSL -u admin:admin "https://localhost:9200/graph/v1beta1/roleManagement/permissions/roleDefinitions" | python3 -c "import sys,json; roles=json.load(sys.stdin); [print(r['id'], r['displayName']) for r in roles]" || echo "WARNING: Graph role definitions failed on LOCAL"
344+
345+
echo "=== Diagnostic: listPermissions with Federated filter (admin folder) ==="
346+
ADMIN_HOME_ID=$(curl -kfsSL -u admin:admin "https://localhost:9200/graph/v1.0/me/drives" | python3 -c "import sys,json; d=json.load(sys.stdin); print([x for x in d['value'] if x['driveType']=='personal'][0]['id'])" 2>/dev/null || echo "")
347+
if [ -n "$ADMIN_HOME_ID" ]; then
348+
echo " Drive ID: $ADMIN_HOME_ID"
349+
# Create a test folder to get a valid non-root item ID (space root has no federated roles by design)
350+
curl -kfsSL -u admin:admin -X MKCOL \
351+
"https://localhost:9200/remote.php/dav/spaces/$ADMIN_HOME_ID/federated-role-test/" > /dev/null 2>&1 || true
352+
FOLDER_ITEM_ID=$(curl -kfsSL -u admin:admin \
353+
"https://localhost:9200/graph/v1.0/drives/$ADMIN_HOME_ID/items/root:/federated-role-test:/" \
354+
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "")
355+
if [ -n "$FOLDER_ITEM_ID" ]; then
356+
echo " Folder item ID: $FOLDER_ITEM_ID"
357+
curl -ksSL -u admin:admin \
358+
"https://localhost:9200/graph/v1beta1/drives/$ADMIN_HOME_ID/items/$FOLDER_ITEM_ID/permissions?\$filter=@libre.graph.permissions.roles.allowedValues/rolePermissions/any(p:contains(p/condition,%20'@Subject.UserType%3D%3D%22Federated%22'))&\$select=@libre.graph.permissions.roles.allowedValues" \
359+
| python3 -c "import sys,json; d=json.load(sys.stdin); roles=d.get('@libre.graph.permissions.roles.allowedValues',[]); [print(r['id'], r.get('displayName','?')) for r in roles] or print(' (empty roles list)')" \
360+
2>/dev/null || echo "WARNING: listPermissions with Federated filter failed"
361+
else
362+
echo "WARNING: Could not get folder item ID"
363+
fi
364+
else
365+
echo "WARNING: Could not get admin home drive ID"
366+
fi
340367
fi

tests/drone/providers.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,27 @@
2020
"host": ".*:9200"
2121
}
2222
]
23+
},
24+
{
25+
"name": "ocis-federated",
26+
"full_name": "ocis federated server",
27+
"organization": "ownCloud",
28+
"domain": ".*:10200",
29+
"homepage": "https://owncloud.com",
30+
"services": [
31+
{
32+
"endpoint": {
33+
"type": {
34+
"name": "OCM",
35+
"description": "ownCloud Open Cloud Mesh API"
36+
},
37+
"name": "ownCloud - OCM API",
38+
"path": "https://.*:10200/ocm/",
39+
"is_monitored": true
40+
},
41+
"api_version": "0.0.1",
42+
"host": ".*:10200"
43+
}
44+
]
2345
}
2446
]

tests/e2e/support/objects/app-files/search/actions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,15 @@ export const toggleSearchTitleOnly = async ({
129129
}): Promise<void> => {
130130
const selector =
131131
enableOrDisable === 'enable' ? enableSearchTitleOnlySelector : disableSearchTitleOnlySelector
132-
await page.locator(selector).click()
132+
await Promise.all([
133+
page.waitForResponse(
134+
(resp) =>
135+
resp.url().includes('/dav/spaces') &&
136+
resp.status() === 207 &&
137+
resp.request().method() === 'REPORT'
138+
),
139+
page.locator(selector).click()
140+
])
133141
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
134142
page,
135143
['files'],

tests/e2e/support/objects/app-files/share/actions.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,39 @@ export const createShare = async (args: createShareArgs): Promise<void> => {
101101
}
102102
const federatedShare = recipients[0].shareType
103103
if (federatedShare) {
104-
await Promise.all([
105-
locatorUtils.waitForEvent(page.locator(invitePanel), 'transitionend'),
106-
page.locator(userTypeFilter).click()
107-
])
104+
// --- WHY THIS WORKAROUND EXISTS ---
105+
// The "External users" filter chip (OcFilterChip → OcDrop → Tippy.js) teleports its
106+
// dropdown content to document.body, outside the Vue component tree. Tippy's own
107+
// `close-on-click` handler fires on the toggle BEFORE the bubbled event reaches Vue's
108+
// @click="selectShareRoleType(option)" in InviteCollaboratorForm.vue.
109+
// With the original page.locator(userTypeFilter).click(), isExternalShareRoleType stayed
110+
// false: the invite input searched all users (not Federated), Brian Murphy was not found.
111+
//
112+
// Fix: open the dropdown via Tippy's JS API (btn._tippy.show()) — bypasses Playwright's
113+
// synthetic event path. Then fire the item click via dispatchEvent({bubbles:true})
114+
// directly on the DOM node so it reaches Vue's handler before Tippy can intercept.
115+
//
116+
await page.evaluate(() => {
117+
const btn = document.querySelector(
118+
'.invite-form-share-role-type .oc-filter-chip-button'
119+
) as any
120+
btn?._tippy?.show()
121+
})
122+
await page.locator(userTypeSelector).filter({ hasText: federatedShare }).waitFor()
108123
await objects.a11y.Accessibility.assertNoSevereA11yViolations(page, ['tippyBox'], 'app sidebar')
109-
await page.locator(userTypeSelector).filter({ hasText: federatedShare }).click()
124+
// Use JS dispatchEvent — Playwright .click() on teleported Tippy content may not reach Vue handlers
125+
await page.evaluate((shareType) => {
126+
const items = Array.from(document.querySelectorAll('.invite-form-share-role-type-item'))
127+
const target = items.find((el) =>
128+
el.textContent?.toLowerCase().includes(shareType.toLowerCase())
129+
) as HTMLElement
130+
target?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
131+
}, federatedShare)
132+
// Small wait for Vue reactivity to update
133+
await page.waitForTimeout(200)
110134
}
111-
await Collaborator.inviteCollaborators({ page, collaborators: recipients })
112135

136+
await Collaborator.inviteCollaborators({ page, collaborators: recipients })
113137
await sidebar.close({ page })
114138
}
115139

tests/e2e/support/objects/app-files/share/collaborator.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,18 @@ export default class Collaborator {
132132
})
133133
)
134134
}
135-
await Promise.all([...checkResponses, page.locator(Collaborator.sendInvitationButton).click()])
135+
// --- WHY THIS WORKAROUND EXISTS ---
136+
// The Share button (#new-collaborators-form-create-button) sits inside vue-select's
137+
// vs__actions div. Playwright's page.locator(...).click() did not reach Vue's
138+
// @click="share" handler — POST /graph/.../invite was never made, waitForResponse
139+
// timed out after 30 s. dispatchEvent fires the click directly on the DOM node.
140+
await Promise.all([
141+
...checkResponses,
142+
page.evaluate(() => {
143+
const btn = document.querySelector('#new-collaborators-form-create-button') as HTMLElement
144+
btn?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
145+
})
146+
])
136147
}
137148

138149
static async inviteCollaborators(args: InviteCollaboratorsArgs): Promise<void> {
@@ -161,9 +172,38 @@ export default class Collaborator {
161172
dropdownSelector = Collaborator.newCollaboratorRoleDropdown
162173
itemSelector = Collaborator.collaboratorRoleButton
163174
}
164-
await page.locator(dropdownSelector).click()
175+
// --- WHY THIS WORKAROUND EXISTS ---
176+
// The role dropdown (#files-collaborators-role-button-new) is a Tippy toggle inside
177+
// vue-select's vs__actions container. Same mechanism as the filter chip in actions.ts:
178+
// Playwright .click() is intercepted by Tippy's close-on-click before Vue's
179+
// @option-change="collaboratorRoleChanged" fires — the role never changes, and the test
180+
// timed out waiting for //button[contains(@id,"fb6c3e19-...")] (the selected-role indicator).
181+
//
182+
// Fix: open via _tippy.show(), then fire via dispatchEvent on the role button.
183+
// Note: itemSelector is an XPath expression like '//button[contains(@id,"fb6c3e19-...")]'.
184+
// document.querySelector() rejects XPath syntax (throws DOMException) —
185+
// document.evaluate() is required to resolve XPath nodes.
186+
const toggleId = await page.locator(dropdownSelector).getAttribute('id')
187+
await page.evaluate((id) => {
188+
const btn = id
189+
? document.getElementById(id)
190+
: (document.querySelector('[id^="files-collaborators-role-button"]') as any)
191+
btn?._tippy?.show()
192+
}, toggleId)
165193
await objects.a11y.Accessibility.assertNoSevereA11yViolations(page, ['tippyBox'], 'tippy box')
166-
await page.locator(util.format(itemSelector, role)).click()
194+
// dispatchEvent since Tippy close-on-click intercepts .click() before Vue handler fires
195+
const roleSelector = util.format(itemSelector, role)
196+
await page.locator(roleSelector).waitFor()
197+
await page.evaluate((xpathSelector) => {
198+
const btn = document.evaluate(
199+
xpathSelector,
200+
document,
201+
null,
202+
XPathResult.FIRST_ORDERED_NODE_TYPE,
203+
null
204+
).singleNodeValue as HTMLElement
205+
btn?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
206+
}, roleSelector)
167207
}
168208

169209
static async changeCollaboratorRole(args: CollaboratorArgs): Promise<void> {

0 commit comments

Comments
 (0)