demo-todo-app.spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // @ts-check
  2. import { test, expect } from '@playwright/test';
  3. test.beforeEach(async ({ page }) => {
  4. await page.goto('https://demo.playwright.dev/todomvc');
  5. });
  6. const TODO_ITEMS = [
  7. 'buy some cheese',
  8. 'feed the cat',
  9. 'book a doctors appointment'
  10. ];
  11. test.describe('New Todo', () => {
  12. test('should allow me to add todo items', async ({ page }) => {
  13. // create a new todo locator
  14. const newTodo = page.getByPlaceholder('What needs to be done?');
  15. // Create 1st todo.
  16. await newTodo.fill(TODO_ITEMS[0]);
  17. await newTodo.press('Enter');
  18. // Make sure the list only has one todo item.
  19. await expect(page.getByTestId('todo-title')).toHaveText([
  20. TODO_ITEMS[0]
  21. ]);
  22. // Create 2nd todo.
  23. await newTodo.fill(TODO_ITEMS[1]);
  24. await newTodo.press('Enter');
  25. // Make sure the list now has two todo items.
  26. await expect(page.getByTestId('todo-title')).toHaveText([
  27. TODO_ITEMS[0],
  28. TODO_ITEMS[1]
  29. ]);
  30. await checkNumberOfTodosInLocalStorage(page, 2);
  31. });
  32. test('should clear text input field when an item is added', async ({ page }) => {
  33. // create a new todo locator
  34. const newTodo = page.getByPlaceholder('What needs to be done?');
  35. // Create one todo item.
  36. await newTodo.fill(TODO_ITEMS[0]);
  37. await newTodo.press('Enter');
  38. // Check that input is empty.
  39. await expect(newTodo).toBeEmpty();
  40. await checkNumberOfTodosInLocalStorage(page, 1);
  41. });
  42. test('should append new items to the bottom of the list', async ({ page }) => {
  43. // Create 3 items.
  44. await createDefaultTodos(page);
  45. // create a todo count locator
  46. const todoCount = page.getByTestId('todo-count')
  47. // Check test using different methods.
  48. await expect(page.getByText('3 items left')).toBeVisible();
  49. await expect(todoCount).toHaveText('3 items left');
  50. await expect(todoCount).toContainText('3');
  51. await expect(todoCount).toHaveText(/3/);
  52. // Check all items in one call.
  53. await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
  54. await checkNumberOfTodosInLocalStorage(page, 3);
  55. });
  56. });
  57. test.describe('Mark all as completed', () => {
  58. test.beforeEach(async ({ page }) => {
  59. await createDefaultTodos(page);
  60. await checkNumberOfTodosInLocalStorage(page, 3);
  61. });
  62. test.afterEach(async ({ page }) => {
  63. await checkNumberOfTodosInLocalStorage(page, 3);
  64. });
  65. test('should allow me to mark all items as completed', async ({ page }) => {
  66. // Complete all todos.
  67. await page.getByLabel('Mark all as complete').check();
  68. // Ensure all todos have 'completed' class.
  69. await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
  70. await checkNumberOfCompletedTodosInLocalStorage(page, 3);
  71. });
  72. test('should allow me to clear the complete state of all items', async ({ page }) => {
  73. const toggleAll = page.getByLabel('Mark all as complete');
  74. // Check and then immediately uncheck.
  75. await toggleAll.check();
  76. await toggleAll.uncheck();
  77. // Should be no completed classes.
  78. await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
  79. });
  80. test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
  81. const toggleAll = page.getByLabel('Mark all as complete');
  82. await toggleAll.check();
  83. await expect(toggleAll).toBeChecked();
  84. await checkNumberOfCompletedTodosInLocalStorage(page, 3);
  85. // Uncheck first todo.
  86. const firstTodo = page.getByTestId('todo-item').nth(0);
  87. await firstTodo.getByRole('checkbox').uncheck();
  88. // Reuse toggleAll locator and make sure its not checked.
  89. await expect(toggleAll).not.toBeChecked();
  90. await firstTodo.getByRole('checkbox').check();
  91. await checkNumberOfCompletedTodosInLocalStorage(page, 3);
  92. // Assert the toggle all is checked again.
  93. await expect(toggleAll).toBeChecked();
  94. });
  95. });
  96. test.describe('Item', () => {
  97. test('should allow me to mark items as complete', async ({ page }) => {
  98. // create a new todo locator
  99. const newTodo = page.getByPlaceholder('What needs to be done?');
  100. // Create two items.
  101. for (const item of TODO_ITEMS.slice(0, 2)) {
  102. await newTodo.fill(item);
  103. await newTodo.press('Enter');
  104. }
  105. // Check first item.
  106. const firstTodo = page.getByTestId('todo-item').nth(0);
  107. await firstTodo.getByRole('checkbox').check();
  108. await expect(firstTodo).toHaveClass('completed');
  109. // Check second item.
  110. const secondTodo = page.getByTestId('todo-item').nth(1);
  111. await expect(secondTodo).not.toHaveClass('completed');
  112. await secondTodo.getByRole('checkbox').check();
  113. // Assert completed class.
  114. await expect(firstTodo).toHaveClass('completed');
  115. await expect(secondTodo).toHaveClass('completed');
  116. });
  117. test('should allow me to un-mark items as complete', async ({ page }) => {
  118. // create a new todo locator
  119. const newTodo = page.getByPlaceholder('What needs to be done?');
  120. // Create two items.
  121. for (const item of TODO_ITEMS.slice(0, 2)) {
  122. await newTodo.fill(item);
  123. await newTodo.press('Enter');
  124. }
  125. const firstTodo = page.getByTestId('todo-item').nth(0);
  126. const secondTodo = page.getByTestId('todo-item').nth(1);
  127. const firstTodoCheckbox = firstTodo.getByRole('checkbox');
  128. await firstTodoCheckbox.check();
  129. await expect(firstTodo).toHaveClass('completed');
  130. await expect(secondTodo).not.toHaveClass('completed');
  131. await checkNumberOfCompletedTodosInLocalStorage(page, 1);
  132. await firstTodoCheckbox.uncheck();
  133. await expect(firstTodo).not.toHaveClass('completed');
  134. await expect(secondTodo).not.toHaveClass('completed');
  135. await checkNumberOfCompletedTodosInLocalStorage(page, 0);
  136. });
  137. test('should allow me to edit an item', async ({ page }) => {
  138. await createDefaultTodos(page);
  139. const todoItems = page.getByTestId('todo-item');
  140. const secondTodo = todoItems.nth(1);
  141. await secondTodo.dblclick();
  142. await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
  143. await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
  144. await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
  145. // Explicitly assert the new text value.
  146. await expect(todoItems).toHaveText([
  147. TODO_ITEMS[0],
  148. 'buy some sausages',
  149. TODO_ITEMS[2]
  150. ]);
  151. await checkTodosInLocalStorage(page, 'buy some sausages');
  152. });
  153. });
  154. test.describe('Editing', () => {
  155. test.beforeEach(async ({ page }) => {
  156. await createDefaultTodos(page);
  157. await checkNumberOfTodosInLocalStorage(page, 3);
  158. });
  159. test('should hide other controls when editing', async ({ page }) => {
  160. const todoItem = page.getByTestId('todo-item').nth(1);
  161. await todoItem.dblclick();
  162. await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
  163. await expect(todoItem.locator('label', {
  164. hasText: TODO_ITEMS[1],
  165. })).not.toBeVisible();
  166. await checkNumberOfTodosInLocalStorage(page, 3);
  167. });
  168. test('should save edits on blur', async ({ page }) => {
  169. const todoItems = page.getByTestId('todo-item');
  170. await todoItems.nth(1).dblclick();
  171. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
  172. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
  173. await expect(todoItems).toHaveText([
  174. TODO_ITEMS[0],
  175. 'buy some sausages',
  176. TODO_ITEMS[2],
  177. ]);
  178. await checkTodosInLocalStorage(page, 'buy some sausages');
  179. });
  180. test('should trim entered text', async ({ page }) => {
  181. const todoItems = page.getByTestId('todo-item');
  182. await todoItems.nth(1).dblclick();
  183. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
  184. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
  185. await expect(todoItems).toHaveText([
  186. TODO_ITEMS[0],
  187. 'buy some sausages',
  188. TODO_ITEMS[2],
  189. ]);
  190. await checkTodosInLocalStorage(page, 'buy some sausages');
  191. });
  192. test('should remove the item if an empty text string was entered', async ({ page }) => {
  193. const todoItems = page.getByTestId('todo-item');
  194. await todoItems.nth(1).dblclick();
  195. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
  196. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
  197. await expect(todoItems).toHaveText([
  198. TODO_ITEMS[0],
  199. TODO_ITEMS[2],
  200. ]);
  201. });
  202. test('should cancel edits on escape', async ({ page }) => {
  203. const todoItems = page.getByTestId('todo-item');
  204. await todoItems.nth(1).dblclick();
  205. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
  206. await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
  207. await expect(todoItems).toHaveText(TODO_ITEMS);
  208. });
  209. });
  210. test.describe('Counter', () => {
  211. test('should display the current number of todo items', async ({ page }) => {
  212. // create a new todo locator
  213. const newTodo = page.getByPlaceholder('What needs to be done?');
  214. // create a todo count locator
  215. const todoCount = page.getByTestId('todo-count')
  216. await newTodo.fill(TODO_ITEMS[0]);
  217. await newTodo.press('Enter');
  218. await expect(todoCount).toContainText('1');
  219. await newTodo.fill(TODO_ITEMS[1]);
  220. await newTodo.press('Enter');
  221. await expect(todoCount).toContainText('2');
  222. await checkNumberOfTodosInLocalStorage(page, 2);
  223. });
  224. });
  225. test.describe('Clear completed button', () => {
  226. test.beforeEach(async ({ page }) => {
  227. await createDefaultTodos(page);
  228. });
  229. test('should display the correct text', async ({ page }) => {
  230. await page.locator('.todo-list li .toggle').first().check();
  231. await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
  232. });
  233. test('should remove completed items when clicked', async ({ page }) => {
  234. const todoItems = page.getByTestId('todo-item');
  235. await todoItems.nth(1).getByRole('checkbox').check();
  236. await page.getByRole('button', { name: 'Clear completed' }).click();
  237. await expect(todoItems).toHaveCount(2);
  238. await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
  239. });
  240. test('should be hidden when there are no items that are completed', async ({ page }) => {
  241. await page.locator('.todo-list li .toggle').first().check();
  242. await page.getByRole('button', { name: 'Clear completed' }).click();
  243. await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
  244. });
  245. });
  246. test.describe('Persistence', () => {
  247. test('should persist its data', async ({ page }) => {
  248. // create a new todo locator
  249. const newTodo = page.getByPlaceholder('What needs to be done?');
  250. for (const item of TODO_ITEMS.slice(0, 2)) {
  251. await newTodo.fill(item);
  252. await newTodo.press('Enter');
  253. }
  254. const todoItems = page.getByTestId('todo-item');
  255. const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
  256. await firstTodoCheck.check();
  257. await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
  258. await expect(firstTodoCheck).toBeChecked();
  259. await expect(todoItems).toHaveClass(['completed', '']);
  260. // Ensure there is 1 completed item.
  261. await checkNumberOfCompletedTodosInLocalStorage(page, 1);
  262. // Now reload.
  263. await page.reload();
  264. await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
  265. await expect(firstTodoCheck).toBeChecked();
  266. await expect(todoItems).toHaveClass(['completed', '']);
  267. });
  268. });
  269. test.describe('Routing', () => {
  270. test.beforeEach(async ({ page }) => {
  271. await createDefaultTodos(page);
  272. // make sure the app had a chance to save updated todos in storage
  273. // before navigating to a new view, otherwise the items can get lost :(
  274. // in some frameworks like Durandal
  275. await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
  276. });
  277. test('should allow me to display active items', async ({ page }) => {
  278. const todoItem = page.getByTestId('todo-item');
  279. await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
  280. await checkNumberOfCompletedTodosInLocalStorage(page, 1);
  281. await page.getByRole('link', { name: 'Active' }).click();
  282. await expect(todoItem).toHaveCount(2);
  283. await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
  284. });
  285. test('should respect the back button', async ({ page }) => {
  286. const todoItem = page.getByTestId('todo-item');
  287. await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
  288. await checkNumberOfCompletedTodosInLocalStorage(page, 1);
  289. await test.step('Showing all items', async () => {
  290. await page.getByRole('link', { name: 'All' }).click();
  291. await expect(todoItem).toHaveCount(3);
  292. });
  293. await test.step('Showing active items', async () => {
  294. await page.getByRole('link', { name: 'Active' }).click();
  295. });
  296. await test.step('Showing completed items', async () => {
  297. await page.getByRole('link', { name: 'Completed' }).click();
  298. });
  299. await expect(todoItem).toHaveCount(1);
  300. await page.goBack();
  301. await expect(todoItem).toHaveCount(2);
  302. await page.goBack();
  303. await expect(todoItem).toHaveCount(3);
  304. });
  305. test('should allow me to display completed items', async ({ page }) => {
  306. await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
  307. await checkNumberOfCompletedTodosInLocalStorage(page, 1);
  308. await page.getByRole('link', { name: 'Completed' }).click();
  309. await expect(page.getByTestId('todo-item')).toHaveCount(1);
  310. });
  311. test('should allow me to display all items', async ({ page }) => {
  312. await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
  313. await checkNumberOfCompletedTodosInLocalStorage(page, 1);
  314. await page.getByRole('link', { name: 'Active' }).click();
  315. await page.getByRole('link', { name: 'Completed' }).click();
  316. await page.getByRole('link', { name: 'All' }).click();
  317. await expect(page.getByTestId('todo-item')).toHaveCount(3);
  318. });
  319. test('should highlight the currently applied filter', async ({ page }) => {
  320. await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
  321. //create locators for active and completed links
  322. const activeLink = page.getByRole('link', { name: 'Active' });
  323. const completedLink = page.getByRole('link', { name: 'Completed' });
  324. await activeLink.click();
  325. // Page change - active items.
  326. await expect(activeLink).toHaveClass('selected');
  327. await completedLink.click();
  328. // Page change - completed items.
  329. await expect(completedLink).toHaveClass('selected');
  330. });
  331. });
  332. async function createDefaultTodos(page) {
  333. // create a new todo locator
  334. const newTodo = page.getByPlaceholder('What needs to be done?');
  335. for (const item of TODO_ITEMS) {
  336. await newTodo.fill(item);
  337. await newTodo.press('Enter');
  338. }
  339. }
  340. /**
  341. * @param {import('@playwright/test').Page} page
  342. * @param {number} expected
  343. */
  344. async function checkNumberOfTodosInLocalStorage(page, expected) {
  345. return await page.waitForFunction(e => {
  346. return JSON.parse(localStorage['react-todos']).length === e;
  347. }, expected);
  348. }
  349. /**
  350. * @param {import('@playwright/test').Page} page
  351. * @param {number} expected
  352. */
  353. async function checkNumberOfCompletedTodosInLocalStorage(page, expected) {
  354. return await page.waitForFunction(e => {
  355. return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e;
  356. }, expected);
  357. }
  358. /**
  359. * @param {import('@playwright/test').Page} page
  360. * @param {string} title
  361. */
  362. async function checkTodosInLocalStorage(page, title) {
  363. return await page.waitForFunction(t => {
  364. return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t);
  365. }, title);
  366. }