diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index 17f9dc3e662..70fbce9d4b0 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -18,10 +18,13 @@ jobs: timeout-minutes: 5 if: github.repository == 'PostHog/posthog' name: Determine need to run E2E checks + # Set job outputs to values from filter step + outputs: + shouldTriggerCypress: ${{ steps.changes.outputs.shouldTriggerCypress }} steps: - # For pull requests it's not necessary to checkout the code + # For pull requests it's not necessary to check out the code - uses: dorny/paths-filter@v2 - id: filter + id: changes with: filters: | shouldTriggerCypress: diff --git a/cypress/e2e/insights-duplication.cy.ts b/cypress/e2e/insights-duplication.cy.ts new file mode 100644 index 00000000000..a63cdb410cb --- /dev/null +++ b/cypress/e2e/insights-duplication.cy.ts @@ -0,0 +1,66 @@ +import { urls } from 'scenes/urls' +import { randomString } from '../support/random' +import { decideResponse } from '../fixtures/api/decide' +import { savedInsights, createInsight } from '../productAnalytics' + +// For tests related to trends please check trendsElements.js +describe('Insights', () => { + beforeEach(() => { + cy.intercept('https://app.posthog.com/decide/*', (req) => + req.reply( + decideResponse({ + hogql: true, + 'data-exploration-insights': true, + }) + ) + ) + + cy.visit(urls.insightNew()) + }) + + describe('duplicating insights', () => { + let insightName + beforeEach(() => { + cy.visit(urls.savedInsights()) // make sure turbo mode has cached this page + insightName = randomString('insight-name-') + createInsight(insightName) + }) + it('can duplicate insights from the insights list view', () => { + cy.visit(urls.savedInsights()) + cy.contains('.saved-insights table tr', insightName).within(() => { + cy.get('[data-attr="more-button"]').click() + }) + cy.get('[data-attr="duplicate-insight-from-list-view"]').click() + cy.contains('.saved-insights table tr', `${insightName} (copy)`).should('exist') + }) + + it('can duplicate insights from the insights card view', () => { + cy.visit(urls.savedInsights()) + cy.contains('.saved-insights .LemonSegmentedButton', 'Cards').click() + cy.contains('.CardMeta', insightName).within(() => { + cy.get('[data-attr="more-button"]').click() + }) + cy.get('[data-attr="duplicate-insight-from-card-list-view"]').click() + cy.contains('.CardMeta', `${insightName} (copy)`).should('exist') + }) + + it('can duplicate from insight view', () => { + cy.get('.page-buttons [data-attr="more-button"]').click() + cy.get('[data-attr="duplicate-insight-from-insight-view"]').click() + cy.get('[data-attr="insight-name"]').should('contain', `${insightName} (copy)`) + + savedInsights.checkInsightIsInListView(`${insightName} (copy)`) + }) + + it('can save insight as a copy', () => { + cy.get('[data-attr="insight-edit-button"]').click() + + cy.get('[data-attr="insight-save-dropdown"]').click() + cy.get('[data-attr="insight-save-as-new-insight"]').click() + cy.get('.ant-modal-content .ant-btn-primary').click() + cy.get('[data-attr="insight-name"]').should('contain', `${insightName} (copy)`) + + savedInsights.checkInsightIsInListView(`${insightName} (copy)`) + }) + }) +}) diff --git a/cypress/e2e/insights-navigation.cy.ts b/cypress/e2e/insights-navigation.cy.ts new file mode 100644 index 00000000000..00ec829321b --- /dev/null +++ b/cypress/e2e/insights-navigation.cy.ts @@ -0,0 +1,187 @@ +import { urls } from 'scenes/urls' +import { randomString } from '../support/random' +import { decideResponse } from '../fixtures/api/decide' +import { insight } from '../productAnalytics' + +const hogQLQuery = `select event, + count() + from events + group by event, + properties.$browser, + person.properties.email + order by count() desc + limit 2` + +// For tests related to trends please check trendsElements.js +describe('Insights', () => { + beforeEach(() => { + cy.intercept('https://app.posthog.com/decide/*', (req) => + req.reply( + decideResponse({ + hogql: true, + 'data-exploration-insights': true, + }) + ) + ) + + cy.visit(urls.insightNew()) + }) + + describe('navigation', () => { + it('can save and load and edit a SQL insight', () => { + insight.newInsight('SQL') + const insightName = randomString('SQL insight') + insight.editName(insightName) + insight.save() + cy.visit(urls.savedInsights()) + cy.contains('.row-name a', insightName).click() + + cy.get('[data-attr="hogql-query-editor"]').should('not.exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + + cy.get('[data-attr="insight-edit-button"]').click() + insight.clickTab('RETENTION') + + cy.get('[data-attr="insight-save-button"]').click() + + cy.get('.RetentionContainer canvas').should('exist') + cy.get('.RetentionTable__Tab').should('have.length', 66) + }) + + describe('opening a new insight directly', () => { + it('can open a new trends insight', () => { + insight.newInsight('TRENDS') + cy.get('.trends-insights-container canvas').should('exist') + cy.get('tr').should('have.length.gte', 2) + }) + + it('can open a new funnels insight', () => { + insight.newInsight('FUNNELS') + cy.get('.funnels-empty-state__title').should('exist') + }) + + it.skip('can open a new retention insight', () => { + insight.newInsight('RETENTION') + cy.get('.RetentionContainer canvas').should('exist') + cy.get('.RetentionTable__Tab').should('have.length', 66) + }) + + it('can open a new paths insight', () => { + insight.newInsight('PATHS') + cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately + }) + + it('can open a new stickiness insight', () => { + insight.newInsight('STICKINESS') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new lifecycle insight', () => { + insight.newInsight('LIFECYCLE') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new SQL insight', () => { + insight.newInsight('SQL') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + }) + }) + + describe('opening a new insight after opening a new SQL insight', () => { + // TRICKY: these tests have identical assertions to the ones above, but we need to open a SQL insight first + // and then click a different tab to switch to that insight. + // this is because we had a bug where doing that would mean after starting to load the new insight, + // the SQL insight would be unexpectedly re-selected and the page would switch back to it + + beforeEach(() => { + insight.newInsight('SQL') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + }) + + it('can open a new trends insight', () => { + insight.clickTab('TRENDS') + cy.get('.trends-insights-container canvas').should('exist') + cy.get('tr').should('have.length.gte', 2) + cy.contains('tr', 'No insight results').should('not.exist') + }) + + it('can open a new funnels insight', () => { + insight.clickTab('FUNNELS') + cy.get('.funnels-empty-state__title').should('exist') + }) + + it('can open a new retention insight', () => { + insight.clickTab('RETENTION') + cy.get('.RetentionContainer canvas').should('exist') + cy.get('.RetentionTable__Tab').should('have.length', 66) + }) + + it('can open a new paths insight', () => { + insight.clickTab('PATH') + cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately + }) + + it('can open a new stickiness insight', () => { + insight.clickTab('STICKINESS') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new lifecycle insight', () => { + insight.clickTab('LIFECYCLE') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new SQL insight', () => { + insight.clickTab('SQL') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + }) + }) + + it('can open a new SQL insight and navigate to a different one, then back to SQL, and back again', () => { + /** + * This is here as a regression test. We had a bug where navigating to a new query based insight, + * then clicking on the trends tab, then on SQL, and again on trends would mean that the trends + * tab would be selected, but no data loaded for it 🤷‍♀️ + */ + + insight.newInsight('SQL') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + + cy.get('.DataTable tr').should('have.length.gte', 2) + + insight.clickTab('TRENDS') + cy.get('.trends-insights-container canvas').should('exist') + cy.get('tr').should('have.length.gte', 2) + cy.contains('tr', 'No insight results').should('not.exist') + + insight.clickTab('SQL') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + + cy.get('.DataTable tr').should('have.length.gte', 2) + + insight.clickTab('TRENDS') + cy.get('.trends-insights-container canvas').should('exist') + cy.get('tr').should('have.length.gte', 2) + cy.contains('tr', 'No insight results').should('not.exist') + }) + + it('can open event explorer as an insight', () => { + cy.clickNavMenu('events') + cy.get('[data-attr="open-json-editor-button"]').click() + cy.get('[data-attr="insight-json-tab"]').should('exist') + }) + + it('does not show the json tab usually', () => { + cy.clickNavMenu('savedinsights') + cy.get('[data-attr="insight-json-tab"]').should('not.exist') + }) + }) +}) diff --git a/cypress/e2e/insights-unsaved-confirmation.cy.ts b/cypress/e2e/insights-unsaved-confirmation.cy.ts new file mode 100644 index 00000000000..cab74b75d43 --- /dev/null +++ b/cypress/e2e/insights-unsaved-confirmation.cy.ts @@ -0,0 +1,67 @@ +import { urls } from 'scenes/urls' +import { randomString } from '../support/random' +import { decideResponse } from '../fixtures/api/decide' +import { insight } from '../productAnalytics' + +// For tests related to trends please check trendsElements.js +describe('Insights', () => { + beforeEach(() => { + cy.intercept('https://app.posthog.com/decide/*', (req) => + req.reply( + decideResponse({ + hogql: true, + 'data-exploration-insights': true, + }) + ) + ) + + cy.visit(urls.insightNew()) + }) + + describe('unsaved insights confirmation', () => { + it('can move away from an unchanged new insight without confirm()', () => { + insight.newInsight() + cy.log('Navigate away') + cy.get('[data-attr="menu-item-featureflags"]').click() + cy.log('We should be on the Feature Flags page now') + cy.url().should('include', '/feature_flags') + }) + + it('Can navigate away from unchanged saved insight without confirm()', () => { + const insightName = randomString('to save and then navigate away from') + insight.create(insightName) + + cy.get('[data-attr="menu-item-annotations"]').click() + + // the annotations API call is made before the annotations page loads, so we can't wait for it + cy.get('[data-attr="annotations-content"]').should('exist') + cy.url().should('include', '/annotations') + }) + + it('Can keep editing changed new insight after navigating away with confirm() rejection (case 1)', () => { + cy.on('window:confirm', () => { + return false + }) + + insight.newInsight() + cy.log('Add series') + cy.get('[data-attr=add-action-event-button]').click() + cy.log('Navigate away') + cy.get('[data-attr="menu-item-featureflags"]').click() + cy.log('Save button should still be here because case 1 rejects confirm()') + cy.get('[data-attr="insight-save-button"]').should('exist') + }) + + it('Can navigate away from changed new insight with confirm() acceptance (case 2)', () => { + cy.on('window:confirm', () => { + return true + }) + insight.newInsight() + cy.log('Add series') + cy.get('[data-attr=add-action-event-button]').click() + cy.log('Navigate away') + cy.get('[data-attr="menu-item-featureflags"]').click() + cy.url().should('include', '/feature_flags') + }) + }) +}) diff --git a/cypress/e2e/insights.cy.ts b/cypress/e2e/insights.cy.ts index f9b042d6600..5e4fc006385 100644 --- a/cypress/e2e/insights.cy.ts +++ b/cypress/e2e/insights.cy.ts @@ -3,16 +3,8 @@ import { randomString } from '../support/random' import { decideResponse } from '../fixtures/api/decide' import { savedInsights, createInsight, insight } from '../productAnalytics' -const hogQLQuery = `select event, - count() - from events - group by event, - properties.$browser, - person.properties.email - order by count() desc - limit 2` - // For tests related to trends please check trendsElements.js +// insight tests were split up because Cypress was struggling with this many tests in one file🙈 describe('Insights', () => { beforeEach(() => { cy.intercept('https://app.posthog.com/decide/*', (req) => @@ -97,53 +89,6 @@ describe('Insights', () => { savedInsights.checkInsightIsInListView(insightName) }) - describe('unsaved insights confirmation', () => { - it('can move away from an unchanged new insight without confirm()', () => { - insight.newInsight() - cy.log('Navigate away') - cy.get('[data-attr="menu-item-featureflags"]').click() - cy.log('We should be on the Feature Flags page now') - cy.url().should('include', '/feature_flags') - }) - - it('Can navigate away from unchanged saved insight without confirm()', () => { - const insightName = randomString('to save and then navigate away from') - insight.create(insightName) - - cy.get('[data-attr="menu-item-annotations"]').click() - - // the annotations API call is made before the annotations page loads, so we can't wait for it - cy.get('[data-attr="annotations-content"]').should('exist') - cy.url().should('include', '/annotations') - }) - - it('Can keep editing changed new insight after navigating away with confirm() rejection (case 1)', () => { - cy.on('window:confirm', () => { - return false - }) - - insight.newInsight() - cy.log('Add series') - cy.get('[data-attr=add-action-event-button]').click() - cy.log('Navigate away') - cy.get('[data-attr="menu-item-featureflags"]').click() - cy.log('Save button should still be here because case 1 rejects confirm()') - cy.get('[data-attr="insight-save-button"]').should('exist') - }) - - it('Can navigate away from changed new insight with confirm() acceptance (case 2)', () => { - cy.on('window:confirm', () => { - return true - }) - insight.newInsight() - cy.log('Add series') - cy.get('[data-attr=add-action-event-button]').click() - cy.log('Navigate away') - cy.get('[data-attr="menu-item-featureflags"]').click() - cy.url().should('include', '/feature_flags') - }) - }) - it('Shows not found error with invalid short URL', () => { cy.visit('/i/i_dont_exist') cy.location('pathname').should('eq', '/insights/i_dont_exist') @@ -205,210 +150,6 @@ describe('Insights', () => { }) }) - describe('duplicating insights', () => { - let insightName - beforeEach(() => { - cy.visit(urls.savedInsights()) // make sure turbo mode has cached this page - insightName = randomString('insight-name-') - createInsight(insightName) - }) - it('can duplicate insights from the insights list view', () => { - cy.visit(urls.savedInsights()) - cy.contains('.saved-insights table tr', insightName).within(() => { - cy.get('[data-attr="more-button"]').click() - }) - cy.get('[data-attr="duplicate-insight-from-list-view"]').click() - cy.contains('.saved-insights table tr', `${insightName} (copy)`).should('exist') - }) - - it('can duplicate insights from the insights card view', () => { - cy.visit(urls.savedInsights()) - cy.contains('.saved-insights .LemonSegmentedButton', 'Cards').click() - cy.contains('.CardMeta', insightName).within(() => { - cy.get('[data-attr="more-button"]').click() - }) - cy.get('[data-attr="duplicate-insight-from-card-list-view"]').click() - cy.contains('.CardMeta', `${insightName} (copy)`).should('exist') - }) - - it('can duplicate from insight view', () => { - cy.get('.page-buttons [data-attr="more-button"]').click() - cy.get('[data-attr="duplicate-insight-from-insight-view"]').click() - cy.get('[data-attr="insight-name"]').should('contain', `${insightName} (copy)`) - - savedInsights.checkInsightIsInListView(`${insightName} (copy)`) - }) - - it('can save insight as a copy', () => { - cy.get('[data-attr="insight-edit-button"]').click() - - cy.get('[data-attr="insight-save-dropdown"]').click() - cy.get('[data-attr="insight-save-as-new-insight"]').click() - cy.get('.ant-modal-content .ant-btn-primary').click() - cy.get('[data-attr="insight-name"]').should('contain', `${insightName} (copy)`) - - savedInsights.checkInsightIsInListView(`${insightName} (copy)`) - }) - }) - - describe('navigation', () => { - it('can save and load and edit a SQL insight', () => { - insight.newInsight('SQL') - const insightName = randomString('SQL insight') - insight.editName(insightName) - insight.save() - cy.visit(urls.savedInsights()) - cy.contains('.row-name a', insightName).click() - - cy.get('[data-attr="hogql-query-editor"]').should('not.exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - - cy.get('[data-attr="insight-edit-button"]').click() - insight.clickTab('RETENTION') - - cy.get('[data-attr="insight-save-button"]').click() - - cy.get('.RetentionContainer canvas').should('exist') - cy.get('.RetentionTable__Tab').should('have.length', 66) - }) - - describe('opening a new insight directly', () => { - it('can open a new trends insight', () => { - insight.newInsight('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') - cy.get('tr').should('have.length.gte', 2) - }) - - it('can open a new funnels insight', () => { - insight.newInsight('FUNNELS') - cy.get('.funnels-empty-state__title').should('exist') - }) - - it.skip('can open a new retention insight', () => { - insight.newInsight('RETENTION') - cy.get('.RetentionContainer canvas').should('exist') - cy.get('.RetentionTable__Tab').should('have.length', 66) - }) - - it('can open a new paths insight', () => { - insight.newInsight('PATHS') - cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately - }) - - it('can open a new stickiness insight', () => { - insight.newInsight('STICKINESS') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new lifecycle insight', () => { - insight.newInsight('LIFECYCLE') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new SQL insight', () => { - insight.newInsight('SQL') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - }) - }) - - describe('opening a new insight after opening a new SQL insight', () => { - // TRICKY: these tests have identical assertions to the ones above, but we need to open a SQL insight first - // and then click a different tab to switch to that insight. - // this is because we had a bug where doing that would mean after starting to load the new insight, - // the SQL insight would be unexpectedly re-selected and the page would switch back to it - - beforeEach(() => { - insight.newInsight('SQL') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - }) - - it('can open a new trends insight', () => { - insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') - cy.get('tr').should('have.length.gte', 2) - cy.contains('tr', 'No insight results').should('not.exist') - }) - - it('can open a new funnels insight', () => { - insight.clickTab('FUNNELS') - cy.get('.funnels-empty-state__title').should('exist') - }) - - it('can open a new retention insight', () => { - insight.clickTab('RETENTION') - cy.get('.RetentionContainer canvas').should('exist') - cy.get('.RetentionTable__Tab').should('have.length', 66) - }) - - it('can open a new paths insight', () => { - insight.clickTab('PATH') - cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately - }) - - it('can open a new stickiness insight', () => { - insight.clickTab('STICKINESS') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new lifecycle insight', () => { - insight.clickTab('LIFECYCLE') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new SQL insight', () => { - insight.clickTab('SQL') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - }) - }) - - it('can open a new SQL insight and navigate to a different one, then back to SQL, and back again', () => { - /** - * This is here as a regression test. We had a bug where navigating to a new query based insight, - * then clicking on the trends tab, then on SQL, and again on trends would mean that the trends - * tab would be selected, but no data loaded for it 🤷‍♀️ - */ - - insight.newInsight('SQL') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - - cy.get('.DataTable tr').should('have.length.gte', 2) - - insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') - cy.get('tr').should('have.length.gte', 2) - cy.contains('tr', 'No insight results').should('not.exist') - - insight.clickTab('SQL') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - - cy.get('.DataTable tr').should('have.length.gte', 2) - - insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') - cy.get('tr').should('have.length.gte', 2) - cy.contains('tr', 'No insight results').should('not.exist') - }) - - it('can open event explorer as an insight', () => { - cy.clickNavMenu('events') - cy.get('[data-attr="open-json-editor-button"]').click() - cy.get('[data-attr="insight-json-tab"]').should('exist') - }) - - it('does not show the json tab usually', () => { - cy.clickNavMenu('savedinsights') - cy.get('[data-attr="insight-json-tab"]').should('not.exist') - }) - }) - describe('view source', () => { it('can open the query editor', () => { insight.newInsight('TRENDS') diff --git a/cypress/e2e/invites.cy.ts b/cypress/e2e/invites.cy.ts index 009d66f52ed..c05c404ea94 100644 --- a/cypress/e2e/invites.cy.ts +++ b/cypress/e2e/invites.cy.ts @@ -91,8 +91,7 @@ describe('Invite Signup', () => { cy.location('pathname').should('include', 'verify_email') // Log out, log in as main - cy.get('[data-attr=top-menu-toggle]').click() - cy.get('[data-attr=top-menu-item-logout]').click() + cy.visit('/logout') cy.login() // Go to organization settings diff --git a/cypress/e2e/signup.cy.ts b/cypress/e2e/signup.cy.ts index 5cc7cf22c59..af19b96096c 100644 --- a/cypress/e2e/signup.cy.ts +++ b/cypress/e2e/signup.cy.ts @@ -49,7 +49,8 @@ describe('Signup', () => { cy.get('[data-attr=signup-role-at-organization]').contains('Engineering') cy.get('[data-attr=signup-submit]').click() - cy.location('pathname').should('eq', '/verify_email') + // lazy regex for a guid + cy.location('pathname').should('match', /\/verify_email\/[a-zA-Z0-9_.-]*/) }) it('Can fill out all the fields on social login', () => {