Skip to content

Instantly share code, notes, and snippets.

@matchaxnb
Created March 27, 2025 08:04
Show Gist options
  • Save matchaxnb/b487b7a15976b3d7c9049b74576b0e7b to your computer and use it in GitHub Desktop.
Save matchaxnb/b487b7a15976b3d7c9049b74576b0e7b to your computer and use it in GitHub Desktop.
Instructions TD tests webdev

Script pour créer l'appli

# possiblement besoin d'utiliser une version récente de node
nvm use v22.14.0
# pour créer l'appli
npx create-react-app notes-app
cd notes-app
npm install react-router-dom cypress --save

Composants à ajouter

// src/mockAuth.js
export const mockUser = {
  username: 'super.etudiant',
  password: 'password123',
  name: 'Charli XYZ',
  birthDate: '2000-01-01'
};

export const mockLogin = (username, password) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (username === mockUser.username && password === mockUser.password) {
        resolve(mockUser);
      } else {
        reject(new Error('Login failed'));
      }
    }, 500);
  });
};
/* src/components/Login.jsx */
import { useState } from 'react';
import { mockLogin } from '../mockAuth';
import { useNavigate } from 'react-router-dom';

const Login = ({ onLogin }) => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const user = await mockLogin(username, password);
      localStorage.setItem('user', JSON.stringify(user));
      onLogin();
      navigate('/private');
    } catch (err) {
      setError('Login failed');
    }
  };

  return (
    <div>
      <h2>Login</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div>
          <label>Username:</label>
          <input
            value={username}
            type="text" name="username"
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>
        <div>
          <label>Password:</label>
          <input
            type="password"
            name="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

export default Login;
/* src/components/PrivateNotes.jsx */
import { useState, useEffect } from 'react';

const PrivateNotes = () => {
  const [notes, setNotes] = useState(
    localStorage.getItem('notes') || ''
  );
  const user = JSON.parse(localStorage.getItem('user'));

  useEffect(() => {
    const saveNotes = () => {
      localStorage.setItem('notes', notes);
    };
    
    const timer = setInterval(saveNotes, 5000);
    window.addEventListener('beforeunload', saveNotes);

    return () => {
      clearInterval(timer);
      window.removeEventListener('beforeunload', saveNotes);
    };
  }, [notes]);

  return (
    <div>
      <h2>Bonjour {user.name}</h2>
      <p>(mon anniversaire: {user.birthDate})</p>
      <h1>Mes notes</h1>
      <textarea
        value={notes}
        onChange={(e) => setNotes(e.target.value)}
        style={{ width: '100%', height: '300px' }}
      />
    </div>
  );
}
export default PrivateNotes;
/* src/App.js */
import { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './Login';
import PrivateNotes from './PrivateNotes';
import Home from './Home';

export default function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(
    !!localStorage.getItem('user')
  );

  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login onLogin={() => setIsLoggedIn(true)} />} />
        <Route
          path="/private"
          element={
            isLoggedIn ? (
              <PrivateNotes />
            ) : (
              <Navigate to="/login" />
            )
          }
        />
      </Routes>
    </Router>
  );
}
/* src/components/Home.jsx */
import { Link } from 'react-router-dom';

const Home = () => {
  return (
    <div>
      <h1>Notes perso</h1>
      <nav>
        <Link to="/">Accueil</Link> | 
        <Link to="/private">Mes notes personnelles</Link>
      </nav>
    </div>
  );
}
export default Home;

Exemples de tests

/* cypress/e2e/login.cy.js */
describe('Login Flow', () => {
  it('should login with valid credentials', () => {
    cy.visit('/login');
    cy.get('input[name="username"]').type('super.etudiant');
    cy.get('input[name="password"]').type('password123');
    cy.get('button').click();
    cy.url().should('include', '/private');
  });

  it('should show error with invalid credentials', () => {
    cy.visit('/login');
    cy.get('input[type="text"]').type('nimportequoi');
    cy.get('input[type="password"]').type('mauvaismotdepasse');
    cy.get('button').click();
    cy.contains('Login failed');
  });
});
/* cypress/e2e/notes.cy.js */
describe('Notes', () => {
    beforeEach(() => {
      localStorage.setItem('user', JSON.stringify({
        username: 'super.etudiant',
        name: 'Charli XYZ',
        birthDate: '2000-01-01'
      }));
      cy.visit('/private');
    });
  
    it('should persist notes', () => {
      cy.get('textarea').type('Une note pour tester');
      cy.wait(6000); // Wait for autosave
      cy.visit('/')
      cy.wait(1000)
      cy.visit('/private')
      cy.window().then((win) => {
        expect(win.localStorage.getItem('notes')).to.contain('Une note pour tester');
      });
    });
  });

Pour tester avec Cypress:

npx cypress open

Problématisation

Navigation

La navigation n'est présente que sur la home page pour le moment. Changer cela pour qu'elle apparaisse sur toutes les pages. Indice: cela peut se faire en créant un autre composant.

Ajout de fonctionnalité: météo du jour

On va utiliser MirageJS, qui est un framework de mock.

npm install --save-dev miragejs

Ceci ajoute miragejs en dépendance de développement. Cela fait qu'on va l'installer quand on développe, mais qu'on ne va pas l'inclure dans les environnements de production.

Le fichier suivant est un "serveur de mock" Mirage:

/* src/mockServer.js */
import { createServer, Response } from 'miragejs';

const weatherConditions = [
  'sunny', 'cloudy', 'fog', 'light rain', 'heavy rain', 
  'partly cloudy', 'thunderstorms', 'snow'
];

const localNames = {
  en: { Paris: 'Paris', London: 'London', Berlin: 'Berlin' },
  fr: { Paris: 'Paris', London: 'Londres', Berlin: 'Berlin' },
  de: { Paris: 'Paris', London: 'London', Berlin: 'Berlin' }
};

export function makeServer() {
  return createServer({
    routes() {
      this.get('/api/weather', (schema, request) => {
        const location = request.queryParams.location || 'Paris';
        const lang = request.queryParams.lang || 'fr';
        
        // Generate random-ish weather data
        return {
          location: localNames[lang]?.[location] || location,
          temperature: Math.floor(Math.random() * 30 + 273), // 273-303K
          weather: weatherConditions[Math.floor(Math.random() * weatherConditions.length)]
        };
      });
    },
  });
}

On va ajouter une initialisation du serveur de mock si on est en mode développement

/* ajouter dans src/index.js après le dernier import */
import { makeServer } from './mockServer';

// Start mock server in development
if (process.env.NODE_ENV === 'development') {
  makeServer();
}

On va créer un composant d'affichage de la météo

/* src/components/Weather.jsx */
import { useState, useEffect } from 'react';

export default function Weather() {
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // For simplicity, we'll hardcode Paris as location
    const fetchWeather = async () => {
      try {
        const response = await fetch(
          `/api/weather?location=Paris&lang=${navigator.language.split('-')[0]}`
        );
        const data = await response.json();
        setWeather(data);
      } catch (err) {
        setError('Failed to load weather');
      } finally {
        setLoading(false);
      }
    };

    fetchWeather();
  }, []);

  if (loading) return <p>Loading weather...</p>;
  if (error) return <p>{error}</p>;

  return (
    <div>
      <h3>Météo à  {weather.location}</h3>
      <p>Temperature: {Math.round(weather.temperature - 273.15)}°C</p>
      <p>Dehors: {weather.weather}</p>
    </div>
  );
}

On peut maintenant créer le test Cypress adéquat

/* cypress/e2e/weather.cy.js */
describe('Météo', () => {
  beforeEach(() => {
    localStorage.setItem('user', JSON.stringify({
      username: 'super.etudiant',
      name: 'Charli XYZ',
      birthDate: '2000-01-01'
    }));
  });

  it('should display weather information', () => {
    cy.intercept('GET', '/api/weather*', {
      location: 'Saint-Denis',
      temperature: 293,
      weather: 'sunny'
    }).as('weatherRequest');

    cy.visit('/private');
    cy.contains('Current Weather in Test City');
    cy.contains('20°C');
    cy.contains('Conditions: sunny');
  });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment