diff --git a/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx b/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx index 3dd906d5ed8..12e4638be9e 100644 --- a/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx +++ b/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx @@ -90,9 +90,7 @@ function LinkMsUser({ -

- {t('learn.ms.link-signin')} -

+

{t('learn.ms.link-signin')}

) : ( @@ -114,11 +112,9 @@ function LinkMsUser({ -

- {t('learn.ms.unlinked')} -

+

{t('learn.ms.unlinked')}

    -
  1. +
  2. -
  3. - {t('learn.ms.link-li-2')} -
  4. -
  5. - {t('learn.ms.link-li-3')} -
  6. -
  7. - {t('learn.ms.link-li-4')} -
  8. -
  9. +
  10. {t('learn.ms.link-li-2')}
  11. +
  12. {t('learn.ms.link-li-3')}
  13. +
  14. {t('learn.ms.link-li-4')}
  15. +
  16. placeholder
  17. -
  18. - {t('learn.ms.link-li-6')} -
  19. +
  20. {t('learn.ms.link-li-6')}
diff --git a/e2e/link-ms-user.spec.ts b/e2e/link-ms-user.spec.ts index fe3aa3451b6..022982715fa 100644 --- a/e2e/link-ms-user.spec.ts +++ b/e2e/link-ms-user.spec.ts @@ -7,63 +7,115 @@ test.beforeEach(async ({ page }) => { ); }); -test.describe('Link MS user component (unlinked signedOut user)', () => { - test('Component has proper main heading and relevant sections', async ({ +test.describe('Link MS user component (signed-out user)', () => { + test('should display the page content with a signin CTA', async ({ page }) => { - const mainHeading = page.getByRole('heading', { - name: translations.learn.ms['link-header'] - }); - await expect(mainHeading).toBeVisible(); + await expect( + page.getByRole('heading', { + name: 'Trophy - Write Your First Code Using C#', + level: 1 + }) + ).toBeVisible(); - const linkSignInText = page.getByTestId('link-signin-text'); - await expect(linkSignInText).toBeVisible(); + await expect( + page.getByRole('heading', { + name: translations.learn.ms['link-header'], + level: 2 + }) + ).toBeVisible(); + + await expect( + page.getByText(translations.learn.ms['link-signin']) + ).toBeVisible(); + + // There are 2 sign in button on the page: one in the navbar, and one in the page content + const signInButtons = await page + .getByRole('link', { name: translations.buttons['sign-in'] }) + .all(); + expect(signInButtons).toHaveLength(2); }); }); -test.describe('Link MS user component (unlinked signedIn user)', () => { +test.describe('Link MS user component (signed-in user)', () => { test.use({ storageState: 'playwright/.auth/certified-user.json' }); - test('Component has proper main heading and relevant sections', async ({ - page - }) => { - const mainHeading = page.getByRole('heading', { - name: translations.learn.ms['link-header'] - }); - await expect(mainHeading).toBeVisible(); + test("should recognize the user's MS account", async ({ page }) => { + await expect( + page.getByRole('heading', { + name: 'Trophy - Write Your First Code Using C#', + level: 1 + }) + ).toBeVisible(); - const linkSignInText = page.getByTestId('unlinked-text'); - await expect(linkSignInText).toBeVisible(); + await expect( + page.getByText( + 'The Microsoft account with username "certifieduser" is currently linked to your freeCodeCamp account. If this is not your Microsoft username, remove the link.' + ) + ).toBeVisible(); }); - test('Component has proper list of actions', async ({ page }) => { - const linkText1 = page.getByTestId('link-li-1-text'); - await expect(linkText1).toBeVisible(); - await expect(linkText1).toContainText( - 'Using a browser where you are logged into your Microsoft account, go to https://learn.microsoft.com/users/me/transcript' - ); + test('should allow the user to unlink their MS account and display a form for re-link', async ({ + page + }) => { + // Intercept the endpoint to prevent `msUsername` from being deleted + // as the deletion will cause subsequent tests to fail + await page.route('*/**/user/ms-username', async route => { + const json = { msUsername: null }; + await route.fulfill({ json }); + }); - const linkText2 = page.getByTestId('link-li-2-text'); - await expect(linkText2).toBeVisible(); - await expect(linkText2).toHaveText(translations.learn.ms['link-li-2']); + const unlinkButton = page.getByRole('button', { + name: translations.buttons['unlink-account'] + }); + await expect(unlinkButton).toBeVisible(); + await unlinkButton.click(); - const linkText3 = page.getByTestId('link-li-3-text'); - await expect(linkText3).toBeVisible(); - await expect(linkText3).toHaveText(translations.learn.ms['link-li-3']); + await expect( + page + .getByRole('alert') + .filter({ hasText: translations.flash.ms.transcript.unlinked }) + ).toBeVisible(); - const linkText4 = page.getByTestId('link-li-4-text'); - await expect(linkText4).toBeVisible(); - await expect(linkText4).toHaveText(translations.learn.ms['link-li-4']); - - const linkText5 = page.getByTestId('link-li-5-text'); - await expect(linkText5).toBeVisible(); - await expect(linkText5).toHaveText( - 'Paste the URL into the input below, it should look similar to this: https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID' - ); - - const linkText6 = page.getByTestId('link-li-6-text'); - await expect(linkText6).toBeVisible(); - await expect(linkText6).toHaveText(translations.learn.ms['link-li-6']); + await expect( + page.getByRole('heading', { + name: translations.learn.ms['link-header'], + level: 2 + }) + ).toBeVisible(); + await expect(page.getByText(translations.learn.ms.unlinked)).toBeVisible(); + await expect( + page.getByRole('listitem').filter({ + hasText: + 'Using a browser where you are logged into your Microsoft account, go to https://learn.microsoft.com/users/me/transcript' + }) + ).toBeVisible(); + await expect( + page + .getByRole('listitem') + .filter({ hasText: translations.learn.ms['link-li-2'] }) + ).toBeVisible(); + await expect( + page + .getByRole('listitem') + .filter({ hasText: translations.learn.ms['link-li-3'] }) + ).toBeVisible(); + await expect( + page + .getByRole('listitem') + .filter({ hasText: translations.learn.ms['link-li-4'] }) + ).toBeVisible(); + await expect( + page.getByRole('listitem').filter({ + hasText: + 'Paste the URL into the input below, it should look similar to this: https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID' + }) + ).toBeVisible(); + await expect( + page + .getByRole('listitem') + .filter({ hasText: translations.learn.ms['link-li-6'] }) + ).toBeVisible(); const transcriptLinkInput = page.getByLabel( translations.learn.ms['transcript-label'] diff --git a/package.json b/package.json index 7e62413ff31..f483c67a0bf 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,10 @@ "playwright:install-build-tools-linux": "sh ./playwright-install.sh", "rename-challenges": "ts-node tools/challenge-helper-scripts/rename-challenge-files.ts", "seed": "pnpm seed:surveys && pnpm seed:exams && cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user", - "seed:certified-user": "pnpm seed:surveys && pnpm seed:exams && cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user", + "seed:certified-user": "pnpm seed:surveys && pnpm seed:exams && pnpm seed:ms-username && cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user", "seed:exams": "cross-env DEBUG=fcc:* node tools/scripts/seed-exams/create-exams", "seed:surveys": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-surveys", + "seed:ms-username": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-ms-username", "serve:client": "cd ./client && pnpm run serve", "serve:client-ci": "cd ./client && pnpm run serve-ci", "start": "npm-run-all create:shared -p develop:server serve:client", diff --git a/tools/scripts/seed/seed-ms-username.js b/tools/scripts/seed/seed-ms-username.js new file mode 100644 index 00000000000..8b989234d30 --- /dev/null +++ b/tools/scripts/seed/seed-ms-username.js @@ -0,0 +1,74 @@ +const path = require('path'); +const debug = require('debug'); +const { MongoClient, ObjectId } = require('mongodb'); +require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') }); + +const log = debug('fcc:tools:seedMsUsername'); +const { MONGOHQ_URL } = process.env; + +const args = process.argv.slice(2); + +const allowedArgs = ['--delete-only']; + +// Check for invalid arguments +args.forEach(arg => { + if (!allowedArgs.includes(arg)) + throw new Error( + `Invalid argument ${arg}. Allowed arguments are ${allowedArgs.join(', ')}` + ); +}); + +function handleError(err, client) { + if (err) { + console.error('Oh noes!! Error seeding MS username.'); + console.error(err); + try { + client.close(); + } catch (e) { + // no-op + } finally { + /* eslint-disable-next-line no-process-exit */ + process.exit(1); + } + } +} + +const msAccountId = new ObjectId('65785b25d4c5bd0565c0184d'); + +const certifiedUserAccount = { + _id: msAccountId, + userId: new ObjectId('5fa2db00a25c1c1fa49ce067'), + ttl: 77760000000, + msUsername: 'certifieduser' +}; + +const client = new MongoClient(MONGOHQ_URL, { useNewUrlParser: true }); + +log('Connected successfully to mongo'); + +const db = client.db('freecodecamp'); +const msUsername = db.collection('MsUsername'); + +const run = async () => { + if (args.includes('--delete-only')) { + await msUsername.deleteOne({ + _id: { $eq: msAccountId } + }); + + log('MS username deleted'); + return; + } + + // Rewrite if the object exists, create new if it doesn't + await msUsername.updateOne( + { _id: msAccountId }, + { $set: certifiedUserAccount }, + { upsert: true } + ); + + log('MS username seeded'); +}; + +run() + .then(() => client.close()) + .catch(err => handleError(err, client));