diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 5f17738d0..12df0d651 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -76,7 +76,9 @@ const Draft = { }, watch: { editing(newVal) { + console.log('SAVE?', newVal) if (newVal) return + console.log('SAVE', newVal) if (this.safeToSave) { useDraftsStore().addOrSaveDraft(this.draft) } else { diff --git a/src/stores/drafts.js b/src/stores/drafts.js index 8325fba5b..0f3fae527 100644 --- a/src/stores/drafts.js +++ b/src/stores/drafts.js @@ -62,6 +62,7 @@ export const useDraftsStore = defineStore('drafts', { }, async addOrSaveDraft(draft) { const id = draft.id ?? new Date().getTime().toString() + console.log('SAVE', id) const draftWithId = { ...draft, id } this.drafts.set(draft.id, draftWithId) await saveDraftToStorage(draftWithId) diff --git a/test/unit/specs/components/draft.spec.js b/test/unit/specs/components/draft.spec.js deleted file mode 100644 index 9e26e36b3..000000000 --- a/test/unit/specs/components/draft.spec.js +++ /dev/null @@ -1,193 +0,0 @@ -import { createTestingPinia } from '@pinia/testing' -import { flushPromises, mount } from '@vue/test-utils' -import { setActivePinia } from 'pinia' -import { $t, mountOpts, waitForEvent } from 'test/fixtures/setup_test.js' -import { nextTick } from 'vue' - -import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' - -import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useUsersStore } from 'src/stores/users.js' - -const autoSaveOrNot = (caseFn, caseTitle, runFn) => { - caseFn(`${caseTitle} with auto-save`, function () { - return runFn.bind(this)(true) - }) - - caseFn(`${caseTitle} with no auto-save`, function () { - return runFn.bind(this)(false) - }) -} - -const saveManually = async (wrapper) => { - const morePostActions = wrapper.findByText( - 'button', - $t('post_status.more_post_actions'), - ) - await morePostActions.trigger('click') - - const btn = wrapper.findByText( - 'button', - $t('post_status.save_to_drafts_button'), - ) - await btn.trigger('click') -} - -const waitSaveTime = 4000 - -const currentUser = { - id: 'current-user', - default_scope: 'public', - locked: false, -} - -describe('Draft saving', () => { - beforeEach(() => { - setActivePinia(createTestingPinia()) - useUsersStore().currentUser = currentUser - }) - - afterEach(() => { - vi.useRealTimers() - }) - - autoSaveOrNot( - it, - 'should save when the button is clicked', - async (autoSave) => { - const wrapper = mount(PostStatusForm, mountOpts()) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: autoSave, - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - - await saveManually(wrapper) - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal( - 'mew mew', - ) - }, - ) - - it('should auto-save if it is enabled', async function () { - vi.useFakeTimers() - const wrapper = mount(PostStatusForm, mountOpts()) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: true, - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - await vi.advanceTimersByTimeAsync(waitSaveTime) - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew') - }) - - it('should auto-save when close if auto-save is on', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: true, - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await waitForEvent(wrapper, 'close-accepted') - }) - - it('should save when close if auto-save is off, and unsavedPostAction is save', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: false, - unsavedPostAction: 'save', - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await waitForEvent(wrapper, 'close-accepted') - }) - - it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: false, - unsavedPostAction: 'discard', - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - await waitForEvent(wrapper, 'close-accepted') - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - }) - - it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore(createTestingPinia()) - store.mergedConfig = { - autoSaveDraft: false, - unsavedPostAction: 'confirm', - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - await nextTick() - await flushPromises() - const saveButton = await vi.waitFor(() => { - const button = wrapper.findByText( - 'button', - $t('post_status.close_confirm_save_button'), - ) - if (!button) throw new Error('Save button not present') - return button - }) - expect(saveButton).to.be.ok - await saveButton.trigger('click') - console.info('clicked') - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await flushPromises() - await waitForEvent(wrapper, 'close-accepted') - }) -}) diff --git a/test/unit/specs/components/post_status_form.spec.js b/test/unit/specs/components/post_status_form.spec.js index 1fddf4a95..45a6bea22 100644 --- a/test/unit/specs/components/post_status_form.spec.js +++ b/test/unit/specs/components/post_status_form.spec.js @@ -1,16 +1,44 @@ import { createTestingPinia } from '@pinia/testing' -import { mount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { setActivePinia } from 'pinia' -import { mountOpts } from 'test/fixtures/setup_test.js' +import { $t, mountOpts, waitForEvent } from 'test/fixtures/setup_test.js' import { vi } from 'vitest' +import { nextTick } from 'vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import { useDraftsStore } from 'src/stores/drafts.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useStatusesStore } from 'src/stores/statuses.js' import { useUsersStore } from 'src/stores/users.js' +const autoSaveOrNot = (caseFn, caseTitle, runFn) => { + caseFn(`${caseTitle} with auto-save`, function () { + return runFn.bind(this)(true) + }) + + caseFn(`${caseTitle} with no auto-save`, function () { + return runFn.bind(this)(false) + }) +} + +const saveManually = async (wrapper) => { + const morePostActions = wrapper.findByText( + 'button', + $t('post_status.more_post_actions'), + ) + await morePostActions.trigger('click') + + const btn = wrapper.findByText( + 'button', + $t('post_status.save_to_drafts_button'), + ) + await btn.trigger('click') +} + +const waitSaveTime = 4000 + const currentUser = { id: 'current-user', default_scope: 'public', @@ -36,303 +64,476 @@ const repliedStatus2 = { } describe('PostStatusForm', () => { - beforeEach(() => { - vi.useFakeTimers() - setActivePinia(createTestingPinia()) - useUsersStore().currentUser = currentUser - useStatusesStore().allStatuses = new Map([ - [repliedStatus.id, repliedStatus], - ]) - }) + describe('Basic functionality', () => { + beforeEach(() => { + vi.useFakeTimers() + setActivePinia(createTestingPinia()) + useUsersStore().currentUser = currentUser + useStatusesStore().allStatuses = new Map([ + [repliedStatus.id, repliedStatus], + ]) + }) - it('Clean empty initial state', () => { - const wrapper = mount(PostStatusForm, mountOpts()) + afterEach(() => { + vi.useRealTimers() + }) - expect(wrapper.vm.statusType).to.equal('new') - expect(wrapper.vm.newStatus.spoilerText).to.eql('') - expect(wrapper.vm.newStatus.mentions).to.eql('') - expect(wrapper.vm.newStatus.status).to.eql('') - }) + it('Clean empty initial state', () => { + const wrapper = mount(PostStatusForm, mountOpts()) - it('Reset cleans form to pristine state equal to state form was when created', () => { - const wrapper = mount(PostStatusForm, mountOpts()) + expect(wrapper.vm.statusType).to.equal('new') + expect(wrapper.vm.newStatus.spoilerText).to.eql('') + expect(wrapper.vm.newStatus.mentions).to.eql('') + expect(wrapper.vm.newStatus.status).to.eql('') + }) - const initial = { ...wrapper.vm.newStatus } - wrapper.vm.clearStatus() + it('Reset cleans form to pristine state equal to state form was when created', () => { + const wrapper = mount(PostStatusForm, mountOpts()) - expect(wrapper.vm.newStatus).to.eql(initial) - }) + const initial = { ...wrapper.vm.newStatus } + wrapper.vm.clearStatus() - it('Initializes a reply form', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ + expect(wrapper.vm.newStatus).to.eql(initial) + }) + + it('Initializes a reply form', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus, + }, + }), + ) + + useInstanceCapabilitiesStore().quotingAvailable = true + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.refId).to.equal('status-1') + expect(wrapper.vm.quotable).to.equal(true) + expect(wrapper.vm.inReplyToStatusId).to.equal('status-1') + expect(wrapper.vm.newStatus.quote).to.be.null + expect(wrapper.vm.newStatus.poll).to.be.null + expect(wrapper.vm.newStatus.spoilerText).to.eql('') + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + expect(wrapper.vm.newStatus.visibility).to.eql('public') + }) + + it('Copies scope and subject line, disables quoting for locked posts', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + useInstanceCapabilitiesStore().quotingAvailable = true + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.quotable).to.equal(false) + expect(wrapper.vm.newStatus.quote).to.be.null + expect(wrapper.vm.newStatus.poll).to.be.null + expect(wrapper.vm.newStatus.spoilerText).to.eql('re: subject') + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + expect(wrapper.vm.newStatus.visibility).to.eql('private') + + expect(wrapper.vm.postingOptions.status).to.eql('@replied ') + expect(wrapper.vm.postingOptions.spoilerText).to.eql('re: subject') + expect(wrapper.vm.postingOptions.visibility).to.eql('private') + expect(wrapper.vm.postingOptions.sensitive).to.eql(false) + expect(wrapper.vm.postingOptions.media).to.eql([]) + expect(wrapper.vm.postingOptions.inReplyToStatusId).to.eql('status-2') + expect(wrapper.vm.postingOptions.quoteId).to.be.null + expect(wrapper.vm.postingOptions.contentType).to.eql('text/plain') + expect(wrapper.vm.postingOptions.poll).to.be.null + }) + + it('Forces direct mode when replying to a DM, mastodon style subject handling', () => { + // We need to initialize pinia first which is happening here... + const options = mountOpts({ props: { - repliedStatus: repliedStatus, + repliedStatus: { ...repliedStatus2, visibility: 'direct' }, }, - }), - ) + }) - useInstanceCapabilitiesStore().quotingAvailable = true + // ...set our settings... + useMergedConfigStore().mergedConfig = { + ...useMergedConfigStore().mergedConfig, + subjectLineBehavior: 'masto', + } - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.refId).to.equal('status-1') - expect(wrapper.vm.quotable).to.equal(true) - expect(wrapper.vm.inReplyToStatusId).to.equal('status-1') - expect(wrapper.vm.newStatus.quote).to.be.null - expect(wrapper.vm.newStatus.poll).to.be.null - expect(wrapper.vm.newStatus.spoilerText).to.eql('') - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - expect(wrapper.vm.newStatus.visibility).to.eql('public') + // ...and only then mount our component + const wrapper = mount(PostStatusForm, options) + + // Otherwise we get multiple instances of pinia that don't talk to each other + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.quotable).to.equal(false) + expect(wrapper.vm.newStatus.quote).to.be.null + expect(wrapper.vm.newStatus.poll).to.be.null + expect(wrapper.vm.newStatus.spoilerText).to.eql('subject') + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + expect(wrapper.vm.newStatus.visibility).to.eql('direct') + }) + + it('Sets status to statusText without mentions if mentions line is enabled', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + statusText: 'testing', + mentionsLine: true, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('testing') + }) + + it('Sets mention when asked for it', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + profileMention: repliedUser, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('mention') + expect(wrapper.vm.isReply).to.equal(false) + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + }) + + it('Initializes quote when reply/quote toggled to quote', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + + wrapper.vm.quoteThreadToggled = true + + expect(wrapper.vm.newStatus.quote).to.eql({ + thread: true, + id: 'status-2', + }) + }) + + it('Resets quote when reply/quote toggled to reply', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + + wrapper.vm.quoteThreadToggled = true + wrapper.vm.quoteThreadToggled = false + + expect(wrapper.vm.newStatus.quote).to.be.null + }) + + it('Initializes and reset quote when toggling quote attachment', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + + wrapper.vm.toggleQuoteForm() + expect(wrapper.vm.newStatus.quote).to.eql({ + thread: false, + id: null, + url: '', + }) + wrapper.vm.toggleQuoteForm() + expect(wrapper.vm.newStatus.quote).to.be.null + }) + + it('Status editing', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + statusId: 'edited', + statusText: 'text', + statusSubject: 'heading', + statusIsSensitive: true, + statusPoll: {}, + statusQuote: {}, + statusFiles: [], + statusMediaDescriptions: {}, + statusVisibility: 'unlisted', + statusContentType: 'text/markdown', + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('edit') + expect(wrapper.vm.isReply).to.equal(false) // edits don't support changing reply-to so it's pretty much ignored + expect(wrapper.vm.isEdit).to.equal(true) + expect(wrapper.vm.newStatus.quote).to.eql({}) + expect(wrapper.vm.newStatus.poll).to.eql({}) + expect(wrapper.vm.newStatus.spoilerText).to.eql('heading') + expect(wrapper.vm.newStatus.mentions).to.eql('') + expect(wrapper.vm.newStatus.status).to.eql('text') + expect(wrapper.vm.newStatus.visibility).to.eql('unlisted') + expect(wrapper.vm.newStatus.contentType).to.eql('text/markdown') + expect(wrapper.vm.newStatus.nsfw).to.equal(true) + expect(wrapper.vm.newStatus.files).to.eql([]) + }) + + it('Posting should reset idempotency key', async () => { + vi.setSystemTime(new Date(2027, 1, 1, 13)) + const wrapper = mount(PostStatusForm, mountOpts()) + const oldIdempotency = wrapper.vm.idempotencyKey + + vi.setSystemTime(new Date(2028, 1, 1, 13)) + + wrapper.vm.newStatus.status = 'Testing' + await wrapper.vm.postStatus() + + expect(wrapper.vm.idempotencyKey).to.not.eql(oldIdempotency) + }) }) - it('Copies scope and subject line, disables quoting for locked posts', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) + describe('Attachments', () => { + beforeEach(() => { + vi.useFakeTimers() + setActivePinia(createTestingPinia()) + useUsersStore().currentUser = currentUser + useStatusesStore().allStatuses = new Map([ + [repliedStatus.id, repliedStatus], + ]) + }) - useInstanceCapabilitiesStore().quotingAvailable = true + afterEach(() => { + vi.useRealTimers() + }) - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.quotable).to.equal(false) - expect(wrapper.vm.newStatus.quote).to.be.null - expect(wrapper.vm.newStatus.poll).to.be.null - expect(wrapper.vm.newStatus.spoilerText).to.eql('re: subject') - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - expect(wrapper.vm.newStatus.visibility).to.eql('private') + // TODO Probably better to separate attachment upload/manipulation into its own component? + // we need to upload-on-submit for compression setting anyway + it('Attachments manipulations (moving, adding, removing)', () => { + const wrapper = mount(PostStatusForm, mountOpts()) - expect(wrapper.vm.postingOptions.status).to.eql('@replied ') - expect(wrapper.vm.postingOptions.spoilerText).to.eql('re: subject') - expect(wrapper.vm.postingOptions.visibility).to.eql('private') - expect(wrapper.vm.postingOptions.sensitive).to.eql(false) - expect(wrapper.vm.postingOptions.media).to.eql([]) - expect(wrapper.vm.postingOptions.inReplyToStatusId).to.eql('status-2') - expect(wrapper.vm.postingOptions.quoteId).to.be.null - expect(wrapper.vm.postingOptions.contentType).to.eql('text/plain') - expect(wrapper.vm.postingOptions.poll).to.be.null + const i1 = { id: '1', url: 'a' } + const i2 = { id: '2', url: 'b' } + const i3 = { id: '3', url: 'c' } + const i4 = { id: '4', url: 'd' } + const iX = { id: 'x', url: 'x' } + + wrapper.vm.newStatus.files = [i3, i1, iX, i2] + + wrapper.vm.removeMediaFile(iX) + expect(wrapper.vm.newStatus.files).to.eql([i3, i1, i2]) + + wrapper.vm.shiftUpMediaFile(i1) + expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) + + wrapper.vm.shiftUpMediaFile(i1) // should ignore + expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) + + wrapper.vm.shiftDnMediaFile(i3) + expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) + + wrapper.vm.shiftDnMediaFile(i3) // should ignore + expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) + + wrapper.vm.addMediaFile(i4) + expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3, i4]) + }) + + it('Attachment descriptions', () => { + const wrapper = mount(PostStatusForm, mountOpts()) + + const i1 = { id: '1', url: 'a' } + + wrapper.vm.addMediaFile(i1) + expect(wrapper.vm.newStatus.files).to.eql([i1]) + + wrapper.vm.editAttachment(i1, 'description') + expect(wrapper.vm.newStatus.mediaDescriptions['1']).to.eql('description') + }) }) - it('Forces direct mode when replying to a DM, mastodon style subject handling', () => { - // We need to initialize pinia first which is happening here... - const options = mountOpts({ - props: { - repliedStatus: { ...repliedStatus2, visibility: 'direct' }, + describe('Draft saving', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + useUsersStore().currentUser = currentUser + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + autoSaveOrNot( + it, + 'should save when the button is clicked', + async (autoSave) => { + const wrapper = mount(PostStatusForm, mountOpts()) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: autoSave, + } + expect(useDraftsStore().draftsCount).to.equal(0) + + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + + await saveManually(wrapper) + expect(useDraftsStore().draftsCount).to.equal(1) + expect(useDraftsStore().draftsArray[0].status).to.equal('mew mew') }, + ) + + it('should auto-save if it is enabled', async function () { + vi.useFakeTimers() + const wrapper = mount(PostStatusForm, mountOpts()) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: true, + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + + expect(useDraftsStore().draftsCount).to.equal(0) + await vi.advanceTimersByTimeAsync(waitSaveTime) + expect(useDraftsStore().draftsCount).to.equal(1) + expect(useDraftsStore().draftsArray[0].status).to.equal('mew mew') }) - // ...set our settings... - useMergedConfigStore().mergedConfig = { - ...useMergedConfigStore().mergedConfig, - subjectLineBehavior: 'masto', - } - - // ...and only then mount our component - const wrapper = mount(PostStatusForm, options) - - // Otherwise we get multiple instances of pinia that don't talk to each other - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.quotable).to.equal(false) - expect(wrapper.vm.newStatus.quote).to.be.null - expect(wrapper.vm.newStatus.poll).to.be.null - expect(wrapper.vm.newStatus.spoilerText).to.eql('subject') - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - expect(wrapper.vm.newStatus.visibility).to.eql('direct') - }) - - it('Sets status to statusText without mentions if mentions line is enabled', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - statusText: 'testing', - mentionsLine: true, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('testing') - }) - - it('Sets mention when asked for it', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - profileMention: repliedUser, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('mention') - expect(wrapper.vm.isReply).to.equal(false) - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - }) - - it('Initializes quote when reply/quote toggled to quote', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - - wrapper.vm.quoteThreadToggled = true - - expect(wrapper.vm.newStatus.quote).to.eql({ thread: true, id: 'status-2' }) - }) - - it('Resets quote when reply/quote toggled to reply', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - - wrapper.vm.quoteThreadToggled = true - wrapper.vm.quoteThreadToggled = false - - expect(wrapper.vm.newStatus.quote).to.be.null - }) - - it('Initializes and reset quote when toggling quote attachment', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - - wrapper.vm.toggleQuoteForm() - expect(wrapper.vm.newStatus.quote).to.eql({ - thread: false, - id: null, - url: '', + it('should auto-save when close if auto-save is on', async () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: true, + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + expect(useDraftsStore().draftsCount).to.equal(1) + await waitForEvent(wrapper, 'close-accepted') + }) + + it('should save when close if auto-save is off, and unsavedPostAction is save', async () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: false, + unsavedPostAction: 'save', + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + expect(useDraftsStore().draftsCount).to.equal(1) + await waitForEvent(wrapper, 'close-accepted') + }) + + it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: false, + unsavedPostAction: 'discard', + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + await waitForEvent(wrapper, 'close-accepted') + expect(useDraftsStore().draftsCount).to.equal(0) + }) + + it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => { + const store = useMergedConfigStore() + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + store.mergedConfig = { + autoSaveDraft: false, + unsavedPostAction: 'confirm', + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + await nextTick() + await flushPromises() + const saveButton = await vi.waitFor(() => { + const button = wrapper.findByText( + 'button', + $t('post_status.close_confirm_save_button'), + ) + if (!button) throw new Error('Save button not present') + return button + }) + expect(saveButton).to.be.ok + await saveButton.trigger('click') + console.info('clicked') + expect(useDraftsStore().draftsCount).to.equal(1) + await flushPromises() + await waitForEvent(wrapper, 'close-accepted') }) - wrapper.vm.toggleQuoteForm() - expect(wrapper.vm.newStatus.quote).to.be.null }) - - it('Status editing', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - statusId: 'edited', - statusText: 'text', - statusSubject: 'heading', - statusIsSensitive: true, - statusPoll: {}, - statusQuote: {}, - statusFiles: [], - statusMediaDescriptions: {}, - statusVisibility: 'unlisted', - statusContentType: 'text/markdown', - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('edit') - expect(wrapper.vm.isReply).to.equal(false) // edits don't support changing reply-to so it's pretty much ignored - expect(wrapper.vm.isEdit).to.equal(true) - expect(wrapper.vm.newStatus.quote).to.eql({}) - expect(wrapper.vm.newStatus.poll).to.eql({}) - expect(wrapper.vm.newStatus.spoilerText).to.eql('heading') - expect(wrapper.vm.newStatus.mentions).to.eql('') - expect(wrapper.vm.newStatus.status).to.eql('text') - expect(wrapper.vm.newStatus.visibility).to.eql('unlisted') - expect(wrapper.vm.newStatus.contentType).to.eql('text/markdown') - expect(wrapper.vm.newStatus.nsfw).to.equal(true) - expect(wrapper.vm.newStatus.files).to.eql([]) - }) - - it('Posting should reset idempotency key', async () => { - vi.setSystemTime(new Date(2027, 1, 1, 13)) - const wrapper = mount(PostStatusForm, mountOpts()) - const oldIdempotency = wrapper.vm.idempotencyKey - - vi.setSystemTime(new Date(2028, 1, 1, 13)) - - wrapper.vm.newStatus.status = 'Testing' - await wrapper.vm.postStatus() - - expect(wrapper.vm.idempotencyKey).to.not.eql(oldIdempotency) - }) - - // TODO Probably better to separate attachment upload/manipulation into its own component? - // we need to upload-on-submit for compression setting anyway - it('Attachments manipulations (moving, adding, removing)', () => { - const wrapper = mount(PostStatusForm, mountOpts()) - - const i1 = { id: '1', url: 'a' } - const i2 = { id: '2', url: 'b' } - const i3 = { id: '3', url: 'c' } - const i4 = { id: '4', url: 'd' } - const iX = { id: 'x', url: 'x' } - - wrapper.vm.newStatus.files = [i3, i1, iX, i2] - - wrapper.vm.removeMediaFile(iX) - expect(wrapper.vm.newStatus.files).to.eql([i3, i1, i2]) - - wrapper.vm.shiftUpMediaFile(i1) - expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) - - wrapper.vm.shiftUpMediaFile(i1) // should ignore - expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) - - wrapper.vm.shiftDnMediaFile(i3) - expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) - - wrapper.vm.shiftDnMediaFile(i3) // should ignore - expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) - - wrapper.vm.addMediaFile(i4) - expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3, i4]) - }) - - it('Attachment descriptions', () => { - const wrapper = mount(PostStatusForm, mountOpts()) - - const i1 = { id: '1', url: 'a' } - - wrapper.vm.addMediaFile(i1) - expect(wrapper.vm.newStatus.files).to.eql([i1]) - - wrapper.vm.editAttachment(i1, 'description') - expect(wrapper.vm.newStatus.mediaDescriptions['1']).to.eql('description') - }) - // TODO: Drafts (needs vuex to pinia migration) })