SmartPlugCardWiring.test.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /**
  2. * SmartPlugCard wiring: each control sends the request for THIS plug, and
  3. * the destructive ones (power off, delete) send nothing when cancelled.
  4. */
  5. import { describe, it, expect, beforeEach } from 'vitest';
  6. import { screen, waitFor } from '@testing-library/react';
  7. import userEvent from '@testing-library/user-event';
  8. import { http, HttpResponse } from 'msw';
  9. import { render } from '../utils';
  10. import { server } from '../mocks/server';
  11. import { SmartPlugCard } from '../../components/SmartPlugCard';
  12. import type { SmartPlug } from '../../api/client';
  13. type Sent = { method: string; path: string; body: unknown };
  14. function makePlug(overrides: Partial<SmartPlug> = {}): SmartPlug {
  15. return {
  16. id: 42,
  17. name: 'Wiring Plug',
  18. plug_type: 'tasmota',
  19. ip_address: '192.0.2.10',
  20. ha_entity_id: null,
  21. printer_id: null,
  22. controls_printer_power: true,
  23. enabled: true,
  24. auto_on: false,
  25. auto_off: false,
  26. auto_off_persistent: false,
  27. off_delay_mode: 'time',
  28. off_delay_minutes: 5,
  29. off_temp_threshold: 70,
  30. username: null,
  31. password: null,
  32. power_alert_enabled: false,
  33. schedule_enabled: false,
  34. schedule_on_time: null,
  35. schedule_off_time: null,
  36. last_state: 'ON',
  37. show_in_switchbar: false,
  38. created_at: '2026-01-01T00:00:00Z',
  39. updated_at: '2026-01-01T00:00:00Z',
  40. ...overrides,
  41. } as unknown as SmartPlug;
  42. }
  43. let sent: Sent[];
  44. beforeEach(() => {
  45. sent = [];
  46. const record = async (request: Request) => {
  47. const url = new URL(request.url);
  48. let body: unknown = null;
  49. try {
  50. body = await request.json();
  51. } catch {
  52. body = null;
  53. }
  54. sent.push({ method: request.method, path: url.pathname, body });
  55. };
  56. server.use(
  57. http.get('*/api/v1/smart-plugs/:id/status', () =>
  58. HttpResponse.json({ state: 'ON', reachable: true, device_name: null, energy: null }),
  59. ),
  60. http.get('*/api/v1/printers/', () => HttpResponse.json([])),
  61. http.post('*/api/v1/smart-plugs/:id/control', async ({ request }) => {
  62. await record(request);
  63. return HttpResponse.json({ success: true, action: 'off' });
  64. }),
  65. http.patch('*/api/v1/smart-plugs/:id', async ({ request }) => {
  66. await record(request);
  67. return HttpResponse.json(makePlug());
  68. }),
  69. http.delete('*/api/v1/smart-plugs/:id', async ({ request }) => {
  70. await record(request);
  71. return new HttpResponse(null, { status: 204 });
  72. }),
  73. );
  74. });
  75. const writes = () => sent.filter((s) => s.method !== 'GET');
  76. async function renderReady() {
  77. const user = userEvent.setup();
  78. render(<SmartPlugCard plug={makePlug()} onEdit={() => undefined} />);
  79. // The power buttons are enabled only once the status says the plug is reachable.
  80. const off = await screen.findByRole('button', { name: 'Off' });
  81. await waitFor(() => expect(off).not.toBeDisabled());
  82. return { user, off };
  83. }
  84. function toggleFor(label: string): HTMLInputElement {
  85. const row = screen.getByText(label).parentElement!.parentElement!;
  86. return row.querySelector('input[type="checkbox"]') as HTMLInputElement;
  87. }
  88. describe('SmartPlugCard wiring', () => {
  89. it('Power Off: confirming sends control "off" for this plug', async () => {
  90. const { user, off } = await renderReady();
  91. await user.click(off);
  92. const dialog = await screen.findByText('Turn Off Smart Plug');
  93. expect(dialog).toBeInTheDocument();
  94. await user.click(screen.getByRole('button', { name: 'Turn Off' }));
  95. await waitFor(() => expect(writes()).toHaveLength(1));
  96. expect(writes()[0]).toEqual({ method: 'POST', path: '/api/v1/smart-plugs/42/control', body: { action: 'off' } });
  97. });
  98. it('Power Off: cancelling sends nothing', async () => {
  99. const { user, off } = await renderReady();
  100. await user.click(off);
  101. await screen.findByText('Turn Off Smart Plug');
  102. await user.click(screen.getByRole('button', { name: 'Cancel' }));
  103. await waitFor(() => expect(screen.queryByText('Turn Off Smart Plug')).not.toBeInTheDocument());
  104. expect(writes()).toEqual([]);
  105. });
  106. it('Power On: confirming sends control "on"', async () => {
  107. const { user } = await renderReady();
  108. await user.click(screen.getByRole('button', { name: 'On' }));
  109. await user.click(await screen.findByRole('button', { name: 'Turn On' }));
  110. await waitFor(() => expect(writes()).toHaveLength(1));
  111. expect(writes()[0]).toEqual({ method: 'POST', path: '/api/v1/smart-plugs/42/control', body: { action: 'on' } });
  112. });
  113. it('Delete: confirming sends DELETE for this plug', async () => {
  114. const { user } = await renderReady();
  115. await user.click(screen.getByText('Automation Settings'));
  116. const edit = await screen.findByRole('button', { name: 'Edit' });
  117. await user.click(edit.nextElementSibling as HTMLElement);
  118. await screen.findByText('Delete Smart Plug');
  119. await user.click(screen.getByRole('button', { name: 'Delete' }));
  120. await waitFor(() => expect(writes()).toHaveLength(1));
  121. expect(writes()[0]).toMatchObject({ method: 'DELETE', path: '/api/v1/smart-plugs/42' });
  122. });
  123. it('Delete: cancelling sends nothing', async () => {
  124. const { user } = await renderReady();
  125. await user.click(screen.getByText('Automation Settings'));
  126. const edit = await screen.findByRole('button', { name: 'Edit' });
  127. await user.click(edit.nextElementSibling as HTMLElement);
  128. await screen.findByText('Delete Smart Plug');
  129. await user.click(screen.getByRole('button', { name: 'Cancel' }));
  130. await waitFor(() => expect(screen.queryByText('Delete Smart Plug')).not.toBeInTheDocument());
  131. expect(writes()).toEqual([]);
  132. });
  133. it.each([
  134. ['Auto On', { auto_on: true }],
  135. ['Auto Off', { auto_off: true }],
  136. ['Enabled', { enabled: false }],
  137. ])('toggle "%s" PATCHes only that field', async (label, expected) => {
  138. const { user } = await renderReady();
  139. await user.click(screen.getByText('Automation Settings'));
  140. await screen.findByText(label);
  141. await user.click(toggleFor(label));
  142. await waitFor(() => expect(writes()).toHaveLength(1));
  143. expect(writes()[0]).toMatchObject({ method: 'PATCH', path: '/api/v1/smart-plugs/42' });
  144. expect(writes()[0].body).toEqual(expected);
  145. });
  146. });