test(12.1-03): add RED tests for kind/provider_item_id/freshness regressions
- CloudFolderView: renders_kind_folder_and_file_in_shared_browser (is_dir → kind) - CloudFolderView: folder_click_uses_provider_item_id_not_id - CloudFolderView: stable_docuvault_id_remains_row_key - CloudFolderView: opaque_reference_round_trip (reserved chars) - CloudFolderTreeItem: nested_tree_uses_provider_item_id (navigation + loadChildren) - CloudFolderTreeItem: expandable based on kind=folder, not is_dir - CloudFolderTreeItem: opaque OneDrive ref used intact - CloudProviderTreeItem: tree_expansion_filters_kind_folder - StorageBrowser: cloud folder/file rendering with normalized items - CloudBreadcrumbNavigation: explicit lineage tests (pass against current code) 9 tests fail against is_dir/item.id behavior as expected
This commit is contained in:
@@ -98,3 +98,211 @@ describe('CloudFolderView', () => {
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ── Normalized API shape: kind/provider_item_id regressions ──────────────────
|
||||
|
||||
/**
|
||||
* Realistic CloudItemOut fixtures matching the canonical API (no is_dir field).
|
||||
* Distinct id vs provider_item_id ensures no accidental pass-by-coincidence.
|
||||
* Duplicate display names ("Documents") with distinct IDs/refs confirm
|
||||
* classification cannot rely on name.
|
||||
*/
|
||||
const FOLDER_A = {
|
||||
id: 'docuvault-uuid-folder-a', // DocuVault stable row key
|
||||
provider_item_id: 'provider/ref/folderA', // opaque provider reference
|
||||
name: 'Documents',
|
||||
kind: 'folder',
|
||||
parent_ref: null,
|
||||
content_type: null,
|
||||
size: null,
|
||||
modified_at: null,
|
||||
etag: null,
|
||||
capabilities: {},
|
||||
}
|
||||
const FOLDER_B = {
|
||||
id: 'docuvault-uuid-folder-b', // distinct stable id
|
||||
provider_item_id: 'provider/ref/folderB', // distinct provider ref
|
||||
name: 'Documents', // intentional duplicate display name
|
||||
kind: 'folder',
|
||||
parent_ref: null,
|
||||
content_type: null,
|
||||
size: null,
|
||||
modified_at: null,
|
||||
etag: null,
|
||||
capabilities: {},
|
||||
}
|
||||
const FILE_A = {
|
||||
id: 'docuvault-uuid-file-a',
|
||||
provider_item_id: 'provider/ref/fileA',
|
||||
name: 'report.pdf',
|
||||
kind: 'file',
|
||||
parent_ref: null,
|
||||
content_type: 'application/pdf',
|
||||
size: 12345,
|
||||
modified_at: '2026-01-01T00:00:00Z',
|
||||
etag: '"etag-abc"',
|
||||
capabilities: {},
|
||||
}
|
||||
const FILE_B = {
|
||||
id: 'docuvault-uuid-file-b',
|
||||
provider_item_id: 'provider/ref/fileB',
|
||||
name: 'report.pdf', // intentional duplicate display name
|
||||
kind: 'file',
|
||||
parent_ref: null,
|
||||
content_type: 'application/pdf',
|
||||
size: 99999,
|
||||
modified_at: null,
|
||||
etag: null,
|
||||
capabilities: {},
|
||||
}
|
||||
|
||||
// Opaque-reference fixtures with reserved characters
|
||||
const FOLDER_OPAQUE = {
|
||||
id: 'docuvault-uuid-opaque-folder',
|
||||
provider_item_id: 'providers/path?query=1&other=2#frag with spaces/Unicode/日本語',
|
||||
name: 'Opaque Folder',
|
||||
kind: 'folder',
|
||||
parent_ref: null,
|
||||
content_type: null,
|
||||
size: null,
|
||||
modified_at: null,
|
||||
etag: null,
|
||||
capabilities: {},
|
||||
}
|
||||
|
||||
describe('renders_kind_folder_and_file_in_shared_browser', () => {
|
||||
it('folders prop uses kind=folder items only — not is_dir', async () => {
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [FOLDER_A, FOLDER_B, FILE_A, FILE_B],
|
||||
capabilities: null,
|
||||
freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' },
|
||||
})
|
||||
// Capture props passed to StorageBrowser
|
||||
let capturedFolders = null
|
||||
let capturedFiles = null
|
||||
const CapturingStub = {
|
||||
template: '<div data-test="storage-browser" />',
|
||||
props: ['folders', 'files', 'mode', 'breadcrumb', 'uploadQueue', 'loading',
|
||||
'emptyMessage', 'emptyHint', 'capabilities', 'connectionRoot',
|
||||
'folderFreshness', 'lastRefreshedAt', 'byteAvailability'],
|
||||
mounted() {
|
||||
capturedFolders = this.folders
|
||||
capturedFiles = this.files
|
||||
},
|
||||
}
|
||||
const w = mount(CloudFolderView, {
|
||||
global: { stubs: { StorageBrowser: CapturingStub } },
|
||||
})
|
||||
await flushPromises()
|
||||
// Must classify by kind, not is_dir
|
||||
expect(capturedFolders).not.toBeNull()
|
||||
expect(capturedFolders.length).toBe(2)
|
||||
expect(capturedFolders.every(f => f.kind === 'folder')).toBe(true)
|
||||
expect(capturedFiles.length).toBe(2)
|
||||
expect(capturedFiles.every(f => f.kind === 'file')).toBe(true)
|
||||
// Duplicate display names are distinguished by id, not name
|
||||
expect(capturedFolders[0].id).not.toBe(capturedFolders[1].id)
|
||||
expect(capturedFiles[0].id).not.toBe(capturedFiles[1].id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('folder_click_uses_provider_item_id_not_id', () => {
|
||||
it('folder-navigate event triggers navigation with provider_item_id, not DocuVault id', async () => {
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [FOLDER_A],
|
||||
capabilities: null,
|
||||
freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' },
|
||||
})
|
||||
let navigateHandler = null
|
||||
const CapturingStub = {
|
||||
template: '<div data-test="storage-browser" />',
|
||||
props: ['folders', 'files', 'mode', 'breadcrumb', 'uploadQueue', 'loading',
|
||||
'emptyMessage', 'emptyHint', 'capabilities', 'connectionRoot',
|
||||
'folderFreshness', 'lastRefreshedAt', 'byteAvailability'],
|
||||
emits: ['folder-navigate'],
|
||||
mounted() {
|
||||
// Simulate a folder-navigate event with the first folder item
|
||||
this.$emit('folder-navigate', this.folders[0])
|
||||
},
|
||||
}
|
||||
const w = mount(CloudFolderView, {
|
||||
global: { stubs: { StorageBrowser: CapturingStub } },
|
||||
})
|
||||
await flushPromises()
|
||||
// navigateTo must use provider_item_id for the route param, not DocuVault id
|
||||
// The route must contain provider_item_id value, not the uuid id
|
||||
expect(mockPush).toHaveBeenCalled()
|
||||
const pushArg = mockPush.mock.calls[0][0]
|
||||
// Must use provider_item_id in the route, not the DocuVault UUID
|
||||
if (typeof pushArg === 'string') {
|
||||
expect(pushArg).toContain(FOLDER_A.provider_item_id)
|
||||
expect(pushArg).not.toContain(FOLDER_A.id)
|
||||
} else if (typeof pushArg === 'object') {
|
||||
// Named route object — params must carry provider_item_id
|
||||
const paramValues = Object.values(pushArg.params ?? {})
|
||||
expect(paramValues.some(v => String(v).includes(FOLDER_A.provider_item_id) || v === FOLDER_A.provider_item_id)).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('stable_docuvault_id_remains_row_key', () => {
|
||||
it('folders passed to StorageBrowser retain their DocuVault id for Vue key identity', async () => {
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [FOLDER_A, FOLDER_B],
|
||||
capabilities: null,
|
||||
freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' },
|
||||
})
|
||||
let capturedFolders = null
|
||||
const CapturingStub = {
|
||||
template: '<div data-test="storage-browser" />',
|
||||
props: ['folders', 'files', 'mode', 'breadcrumb', 'uploadQueue', 'loading',
|
||||
'emptyMessage', 'emptyHint', 'capabilities', 'connectionRoot',
|
||||
'folderFreshness', 'lastRefreshedAt', 'byteAvailability'],
|
||||
mounted() { capturedFolders = this.folders },
|
||||
}
|
||||
const w = mount(CloudFolderView, {
|
||||
global: { stubs: { StorageBrowser: CapturingStub } },
|
||||
})
|
||||
await flushPromises()
|
||||
// DocuVault stable ids must be preserved as row identity
|
||||
expect(capturedFolders.map(f => f.id)).toContain('docuvault-uuid-folder-a')
|
||||
expect(capturedFolders.map(f => f.id)).toContain('docuvault-uuid-folder-b')
|
||||
// provider_item_id must also be present on each item
|
||||
expect(capturedFolders.every(f => f.provider_item_id !== undefined)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('opaque_reference_round_trip', () => {
|
||||
it('provider_item_id containing reserved characters is used intact for navigation', async () => {
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [FOLDER_OPAQUE],
|
||||
capabilities: null,
|
||||
freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' },
|
||||
})
|
||||
const CapturingStub = {
|
||||
template: '<div data-test="storage-browser" />',
|
||||
props: ['folders', 'files', 'mode', 'breadcrumb', 'uploadQueue', 'loading',
|
||||
'emptyMessage', 'emptyHint', 'capabilities', 'connectionRoot',
|
||||
'folderFreshness', 'lastRefreshedAt', 'byteAvailability'],
|
||||
emits: ['folder-navigate'],
|
||||
mounted() {
|
||||
this.$emit('folder-navigate', this.folders[0])
|
||||
},
|
||||
}
|
||||
const w = mount(CloudFolderView, {
|
||||
global: { stubs: { StorageBrowser: CapturingStub } },
|
||||
})
|
||||
await flushPromises()
|
||||
// Navigation must use provider_item_id verbatim — no splitting or decoding
|
||||
expect(mockPush).toHaveBeenCalled()
|
||||
const pushArg = mockPush.mock.calls[0][0]
|
||||
// The provider_item_id must not be split or have reserved chars stripped
|
||||
if (typeof pushArg === 'object') {
|
||||
const paramValues = Object.values(pushArg.params ?? {})
|
||||
// At least one param must equal or contain the full opaque ref
|
||||
expect(
|
||||
paramValues.some(v => String(v) === FOLDER_OPAQUE.provider_item_id)
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user