fix(13-11): ShareModal's MIME map stops filing m4a/aac/opus/wma as Documents
Adds the four missing audio extensions to ShareModal.vue's extension-to-
MIME map (m4a->audio/mp4, aac->audio/aac, opus->audio/opus, wma->audio/
x-ms-wma), extracted to an exported module-scope SHARE_MIME_MAP so it's
directly fixture-testable (useFileType.test.ts convention). All three
maps agree that these eight extensions are audio/*: SHARE_MIME_MAP,
archyContentAdapter.ts's classifyByMime (13-06), and content.rs's
auto-filing check, which is prefix-only (mime_type.starts_with("audio/"))
so any correct audio/* value here already satisfies it. Existing four
entries (mp3/flac/ogg/wav) and the generic-fallback behavior for unknown
extensions are unchanged. Whole neode-ui suite green (924/924).
This commit is contained in:
@@ -161,6 +161,57 @@
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Extension → MIME map used when sharing a file that isn't already in the
|
||||
* catalog (`save()` below, `content.add`). Historically listed exactly four
|
||||
* audio extensions (`mp3`/`flac`/`ogg`/`wav`) and left `m4a`/`aac`/`opus`/
|
||||
* `wma` to fall through to the generic `application/octet-stream` fallback
|
||||
* — those four then never routed to the global audio player (which decides
|
||||
* purely on `mime.startsWith('audio/')`, see `usePaidItemViewer.ts`) and
|
||||
* auto-filed to Documents instead of Music (`content.rs`'s paid-download
|
||||
* auto-filing, same `starts_with("audio/")` check) — 13-CONTEXT.md's
|
||||
* landmine, T-13-72. This fix adds the four missing entries; it does not
|
||||
* change the fallback strategy and does not guess at unknown extensions.
|
||||
*
|
||||
* Kept in sync with two other maps that must agree on every audio
|
||||
* extension (13-11 Task 2's own acceptance criterion):
|
||||
* - `archyContentAdapter.ts`'s `classifyByMime`/`AUDIO_EXT_FALLBACK` (13-06)
|
||||
* - `content.rs`'s auto-filing check (`mime_type.starts_with("audio/")`) —
|
||||
* that check is prefix-only, so any correct `audio/*` value here already
|
||||
* agrees with it; the specific `audio/*` strings below are chosen to
|
||||
* match `classifyByMime`'s own test fixtures exactly, so a byte-for-byte
|
||||
* MIME string never diverges between the two even though the node-side
|
||||
* check itself only cares about the prefix.
|
||||
*
|
||||
* Module-scope (not a local const inside `save()`) and exported so it is a
|
||||
* table `archyContentAdapter.test.ts`-style fixture tests can pin directly,
|
||||
* per this task's own `read_first` guidance to follow
|
||||
* `useFileType.test.ts`'s fixture-table convention.
|
||||
*/
|
||||
export const SHARE_MIME_MAP: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
mkv: 'video/x-matroska',
|
||||
mp3: 'audio/mpeg',
|
||||
flac: 'audio/flac',
|
||||
ogg: 'audio/ogg',
|
||||
wav: 'audio/wav',
|
||||
m4a: 'audio/mp4',
|
||||
aac: 'audio/aac',
|
||||
opus: 'audio/opus',
|
||||
wma: 'audio/x-ms-wma',
|
||||
pdf: 'application/pdf',
|
||||
zip: 'application/zip',
|
||||
txt: 'text/plain',
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
@@ -352,17 +403,11 @@ async function save() {
|
||||
// Add if not in catalog
|
||||
if (!itemId) {
|
||||
const ext = props.filename.split('.').pop()?.toLowerCase() || ''
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
|
||||
webp: 'image/webp', mp4: 'video/mp4', webm: 'video/webm', mkv: 'video/x-matroska',
|
||||
mp3: 'audio/mpeg', flac: 'audio/flac', ogg: 'audio/ogg', wav: 'audio/wav',
|
||||
pdf: 'application/pdf', zip: 'application/zip', txt: 'text/plain',
|
||||
}
|
||||
const addRes = await rpcClient.call<{ item: { id: string } }>({
|
||||
method: 'content.add',
|
||||
params: {
|
||||
filename: (props.filepath || props.filename).replace(/^\/+/, ''),
|
||||
mime_type: mimeMap[ext] || 'application/octet-stream',
|
||||
mime_type: SHARE_MIME_MAP[ext] || 'application/octet-stream',
|
||||
description: '',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { SHARE_MIME_MAP } from '../ShareModal.vue'
|
||||
|
||||
// 13-11 Task 2: ShareModal.vue's extension-to-MIME map historically listed
|
||||
// exactly four audio extensions (mp3/flac/ogg/wav) and left
|
||||
// m4a/aac/opus/wma to fall through to `application/octet-stream` — those
|
||||
// four never routed to the global audio player and auto-filed to Documents
|
||||
// instead of Music. This pins all eight audio extensions plus the existing
|
||||
// non-audio entries, following useFileType.test.ts's fixture-table
|
||||
// convention.
|
||||
|
||||
describe('ShareModal SHARE_MIME_MAP', () => {
|
||||
it.each([
|
||||
['mp3', 'audio/mpeg'],
|
||||
['flac', 'audio/flac'],
|
||||
['ogg', 'audio/ogg'],
|
||||
['wav', 'audio/wav'],
|
||||
['m4a', 'audio/mp4'],
|
||||
['aac', 'audio/aac'],
|
||||
['opus', 'audio/opus'],
|
||||
['wma', 'audio/x-ms-wma'],
|
||||
])('maps .%s to the audio MIME %s', (ext, expected) => {
|
||||
expect(SHARE_MIME_MAP[ext]).toBe(expected)
|
||||
})
|
||||
|
||||
it('every one of the eight audio extensions maps to a real audio/* MIME (not application/octet-stream)', () => {
|
||||
const audioExts = ['mp3', 'flac', 'ogg', 'wav', 'm4a', 'aac', 'opus', 'wma']
|
||||
for (const ext of audioExts) {
|
||||
const mime = SHARE_MIME_MAP[ext]
|
||||
expect(mime).toBeDefined()
|
||||
expect(mime!.startsWith('audio/')).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('existing non-audio entries are unchanged', () => {
|
||||
expect(SHARE_MIME_MAP.jpg).toBe('image/jpeg')
|
||||
expect(SHARE_MIME_MAP.mp4).toBe('video/mp4')
|
||||
expect(SHARE_MIME_MAP.pdf).toBe('application/pdf')
|
||||
expect(SHARE_MIME_MAP.zip).toBe('application/zip')
|
||||
expect(SHARE_MIME_MAP.txt).toBe('text/plain')
|
||||
})
|
||||
|
||||
it('an unknown extension is absent from the map — the fix adds coverage, it does not guess', () => {
|
||||
expect(SHARE_MIME_MAP.xyz123).toBeUndefined()
|
||||
})
|
||||
|
||||
it('agrees with archyContentAdapter.ts\'s classifyByMime on all eight audio extensions', async () => {
|
||||
const { classifyByMime } = await import('../../../composables/archyContentAdapter')
|
||||
const audioExts = ['mp3', 'flac', 'ogg', 'wav', 'm4a', 'aac', 'opus', 'wma']
|
||||
for (const ext of audioExts) {
|
||||
const mime = SHARE_MIME_MAP[ext]!
|
||||
expect(classifyByMime({ mime_type: mime, filename: `track.${ext}` })).toBe('audio')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('an audio MIME never opens the lightbox', () => {
|
||||
it('usePaidItemViewer routes every audio/* mime to the global player before its image/video lightbox branch is ever reached', () => {
|
||||
// usePaidItemViewer.ts's routing order is: audio -> global bottom-bar
|
||||
// player (return); image/video -> lightbox (return); anything else ->
|
||||
// browser-tab fallback. The audio branch is checked FIRST and always
|
||||
// returns, so no audio/* mime can ever reach the lightbox branch —
|
||||
// pinned here structurally (mirrors the real predicate, avoids a heavy
|
||||
// component mount) rather than re-implementing usePaidItemViewer's
|
||||
// async RPC/blob flow in a unit test.
|
||||
const audioExts = ['mp3', 'flac', 'ogg', 'wav', 'm4a', 'aac', 'opus', 'wma']
|
||||
for (const ext of audioExts) {
|
||||
const mime = SHARE_MIME_MAP[ext]!
|
||||
const routesToAudioPlayerFirst = mime.startsWith('audio/')
|
||||
expect(routesToAudioPlayerFirst).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user