const _ = require('lodash'); const {mobiledocToLexical} = require('@tryghost/kg-converters'); const models = require('../../../core/server/models'); const {agentProvider, fixtureManager, mockManager, matchers} = require('../../utils/e2e-framework'); const {anyArray, anyBoolean, anyContentVersion, anyEtag, anyLocationFor, anyObject, anyObjectId, anyISODateTime, anyString, anyUuid} = matchers; const tierSnapshot = { id: anyObjectId, created_at: anyISODateTime, updated_at: anyISODateTime }; const matchPageShallowIncludes = { id: anyObjectId, uuid: anyUuid, comment_id: anyString, url: anyString, authors: anyArray, primary_author: anyObject, tags: anyArray, primary_tag: anyObject, tiers: Array(2).fill(tierSnapshot), created_at: anyISODateTime, updated_at: anyISODateTime, published_at: anyISODateTime, show_title_and_feature_image: anyBoolean }; describe('Pages API', function () { let agent; before(async function () { mockManager.mockLabsEnabled('collectionsCard'); agent = await agentProvider.getAdminAPIAgent(); await fixtureManager.init('posts'); await agent.loginAsOwner(); }); afterEach(function () { mockManager.restore(); }); describe('Read', function () { it('Re-renders html when null', async function () { // "queue" an existing page for re-render as happens when a published page is updated/destroyed const page = await models.Post.findOne({slug: 'static-page-test'}); // NOTE: re-rendering only occurs for lexical pages const lexical = mobiledocToLexical(page.get('mobiledoc')); await models.Base.knex.raw('UPDATE posts set html=NULL, mobiledoc=NULL, lexical=? WHERE id=?', [lexical, page.id]); await agent .get(`/pages/${page.id}/?formats=mobiledoc,lexical,html`) .expectStatus(200) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes)] }); }); }); describe('Browse', function () { it('Re-renders html when null', async function () { // convert inserted pages to lexical and set html=null so we can test re-render const pages = await models.Post.where('type', 'page').fetchAll(); for (const page of pages) { if (!page.get('mobiledoc')) { continue; } const lexical = mobiledocToLexical(page.get('mobiledoc')); await models.Base.knex.raw('UPDATE posts set html=NULL, mobiledoc=NULL, lexical=? WHERE id=?', [lexical, page.id]); } await agent .get('/pages/?formats=mobiledoc,lexical,html') .expectStatus(200) .matchBodySnapshot({ pages: Array(pages.length).fill(Object.assign({}, matchPageShallowIncludes)) }); }); }); describe('Create', function () { it('Can create a page with html', async function () { mockManager.mockLabsDisabled('lexicalEditor'); const page = { title: 'HTML test', html: '
Testing page creation with html
' }; await agent .post('/pages/?source=html&formats=mobiledoc,lexical,html') .body({pages: [page]}) .expectStatus(201) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes, {published_at: null})] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag, location: anyLocationFor('pages') }); }); it('Can create a page with html (labs.lexicalEditor)', async function () { mockManager.mockLabsEnabled('lexicalEditor'); const page = { title: 'HTML test', html: 'Testing page creation with html
' }; await agent .post('/pages/?source=html&formats=mobiledoc,lexical,html') .body({pages: [page]}) .expectStatus(201) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes, {published_at: null})] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag, location: anyLocationFor('pages') }); }); }); describe('Update', function () { it('Can modify show_title_and_feature_image property', async function () { const page = { title: 'Test Page', status: 'draft' }; const {body: pageBody} = await agent .post('/pages/?formats=mobiledoc,lexical,html', { headers: { 'content-type': 'application/json' } }) .body({pages: [page]}) .expectStatus(201); const [pageResponse] = pageBody.pages; await agent .put(`/pages/${pageResponse.id}/?formats=mobiledoc,lexical,html`) .body({ pages: [{ id: pageResponse.id, show_title_and_feature_image: false, // default is true updated_at: pageResponse.updated_at // satisfy collision detection }] }) .expectStatus(200) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes, { published_at: null, show_title_and_feature_image: false })] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag, 'x-cache-invalidate': anyString }); }); it('Works with latest collection card', async function () { const initialLexical = { root: { children: [ { type: 'collection', version: 1, collection: 'latest', postCount: 3, layout: 'grid', columns: 3, header: 'Latest' } ], direction: null, format: '', indent: 0, type: 'root', version: 1 } }; const updatedLexical = _.cloneDeep(initialLexical); updatedLexical.root.children.push({ children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'Testing', type: 'text', version: 1 } ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', version: 1 }); const page = { title: 'Latest Collection Card Test', status: 'draft', lexical: JSON.stringify(initialLexical) }; const {body: createBody} = await agent .post('/pages/?formats=mobiledoc,lexical,html', { headers: { 'content-type': 'application/json' } }) .body({pages: [page]}) .expectStatus(201); const [createResponse] = createBody.pages; // does not match body snapshot as we mostly only care about the request succeeding. // matching body snapshots is tricky because collection cards have dynamic content, // most notably the post dates which are always changing. await agent .put(`/pages/${createResponse.id}/?formats=mobiledoc,lexical,html`) .body({ pages: [{ id: createResponse.id, lexical: JSON.stringify(updatedLexical), updated_at: createResponse.updated_at // satisfy collision detection }] }) .expectStatus(200); }); it('Works with featured collection card', async function () { const initialLexical = { root: { children: [ { type: 'collection', version: 1, collection: 'featured', postCount: 3, layout: 'grid', columns: 3, header: 'Featured' } ], direction: null, format: '', indent: 0, type: 'root', version: 1 } }; const updatedLexical = _.cloneDeep(initialLexical); updatedLexical.root.children.push({ children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'Testing', type: 'text', version: 1 } ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', version: 1 }); const page = { title: 'Latest Collection Card Test', status: 'draft', lexical: JSON.stringify(initialLexical) }; const {body: createBody} = await agent .post('/pages/?formats=mobiledoc,lexical,html', { headers: { 'content-type': 'application/json' } }) .body({pages: [page]}) .expectStatus(201); const [createResponse] = createBody.pages; await agent .put(`/pages/${createResponse.id}/?formats=mobiledoc,lexical,html`) .body({ pages: [{ id: createResponse.id, lexical: JSON.stringify(updatedLexical), updated_at: createResponse.updated_at // satisfy collision detection }] }) .expectStatus(200); }); }); describe('Copy', function () { it('Can copy a page', async function () { const page = { title: 'Test Page', status: 'published' }; const {body: pageBody} = await agent .post('/pages/?formats=mobiledoc,lexical,html', { headers: { 'content-type': 'application/json' } }) .body({pages: [page]}) .expectStatus(201) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes)] }); const [pageResponse] = pageBody.pages; await agent .post(`/pages/${pageResponse.id}/copy?formats=mobiledoc,lexical`) .expectStatus(201) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes, {published_at: null})] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag, location: anyLocationFor('pages') }); }); }); describe('Convert', function () { it('can convert a mobiledoc page to lexical', async function () { const mobiledoc = JSON.stringify({ version: '0.3.1', ghostVersion: '4.0', markups: [], atoms: [], cards: [], sections: [ [1, 'p', [ [0, [], 0, 'This is some great content.'] ]] ] }); const expectedLexical = JSON.stringify({ root: { children: [ { children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'This is some great content.', type: 'text', version: 1 } ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', version: 1 } ], direction: 'ltr', format: '', indent: 0, type: 'root', version: 1 } }); const pageData = { title: 'Test Post', status: 'published', mobiledoc: mobiledoc, lexical: null }; const {body: pageBody} = await agent .post('/pages/?formats=mobiledoc,lexical,html', { headers: { 'content-type': 'application/json' } }) .body({pages: [pageData]}) .expectStatus(201); const [pageResponse] = pageBody.pages; const convertedResponse = await agent .put(`/pages/${pageResponse.id}/?formats=mobiledoc,lexical,html&convert_to_lexical=true`) .body({pages: [Object.assign({}, pageResponse)]}) .expectStatus(200) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes, {lexical: expectedLexical, mobiledoc: null})] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); // rerunning the conversion against a converted post should not change it const convertedPage = convertedResponse.body.pages[0]; const expectedConvertedLexical = convertedPage.lexical; await agent .put(`/pages/${pageResponse.id}/?formats=mobiledoc,lexical,html&convert_to_lexical=true`) .body({pages: [Object.assign({}, convertedPage)]}) .expectStatus(200) .matchBodySnapshot({ pages: [Object.assign({}, matchPageShallowIncludes, {lexical: expectedConvertedLexical, mobiledoc: null})] }) .matchHeaderSnapshot({ 'content-version': anyContentVersion, etag: anyEtag }); }); }); });