codesandbox / sandpack

A component toolkit for creating live-running code editing experiences, using the power of CodeSandbox.
https://sandpack.codesandbox.io
Apache License 2.0
4.78k stars 336 forks source link

SandpackTests load forever when autoReload is false and you Run code before running tests #1005

Open Kukiezi opened 1 year ago

Kukiezi commented 1 year ago

Bug report

Packages affected

Description of the problem

<SandpackTests component loads forever and doesn't run test when autoReload is set to false and you click "Run" code in CodeEditor and then try to run tests.

What were you doing when the problem occurred?

I wanted to set my CodeEditor to not autoReload. When it worked I tried to Run the code from CodeEditor and then run tests. Tests started loading forever instead of running like before.

What steps can we take to reproduce the problem?

https://github.com/codesandbox/sandpack/assets/32630534/ae2ffc96-4c1c-4e9b-a0d9-0ee66df31f22

Link to sandbox: [link]() (optional)

https://codesandbox.io/s/tests-forever-load-kg359q?file=/src/App.js

Your Environment

Software Name/Version
Sandpack-client version
Sandpack-react version 2.7.1
Browser Chrome
Operating System Windows 11
chaoticpotato commented 11 months ago

Hi, Thank you for developing Sandpack, you are all amazing. 🙏 Sorry but is there any progress on this or any ideas to work around this?

Graquick commented 3 months ago

Hey everyone!

Thanks @Kukiezi and @chaoticpotato for your comments. I'm happy to say that I might've come up with a work around this.

I stumbled on this issue while refactoring my custom sandpack setup to be more performant and user friendly. It took a whole week, but I finally got it working, so here it is.

The idea is to create two instances of Sandpack for two purposes: 1. handle everything BUT the tests, 2. handle only the tests. Then there's a state variable that holds the current state of the Sandpack files, and updates them whenever the user runs the code (by pressing the "run" button).

tip: It's good to memoize the return values of our instances with useMemo, in order to control the updates of each instance. This is extremely handy, since it updates the tests whenever the code is ran by the user (which is what we want), but keeps the 1. Sandpack instance (which handles everything BUT the tests) in memory and up to date without having to render every time the user runs the code.


1. App Layout


const initialFiles = {...} // The initial files for your sandpack setup

export default function App() {
  const [files, setFiles] = useState(initialFiles); // State variable for handling the state of the Sandpack files

  return (
    <div className="App">
      <SandpackWithEditor files={files} setFiles={setFiles} /> {/* 1. Sandpack instance for everything BUT tests */}
      <SandpackWithTests files={files} /> {/* 2. Sandpack instance for only tests */}
    </div>
  );
}

2. SandpackWithEditor component

function SandpackWithEditor({ files, setFiles }) {
  const memoEditor = useMemo(() => {
    return (
      <SandpackProvider
        options={{
          autoReload: false,
        }}
        template="vanilla"
        theme="dark"
        files={files}
      >
        <SandpackLayout>
          <SandpackCodeEditor />
          {/*
            The `SandpackPreviewWithConsole` component below 
            is for updating the `files` state with the `sandpack.files`. 
            We get the `sandpack.files` property from the `useSandpack()` hook.
            The `useSandpack()`  hook only works inside a `SandpackProvider`, 
            which is why  we are creating this component .
          */}
          <SandpackPreviewWithConsole setFiles={setFiles} />
        </SandpackLayout>
      </SandpackProvider>
    );
  }, []);

  return memoEditor;
}

3. SandpackPreviewWithConsole component

function SandpackPreviewWithConsole({ setFiles }) {
  const { sandpack } = useSandpack(); // Get the current sandpack state from the `useSandpack()` hook

  // Update the state variable `files` with the `sandpack.files` whenever the user runs sandpack.
  // The `useSandpackRun` is a custom hook that we will create to handle the logic (more about it later)
  // note: `useSandpackRun` takes a handle function as an argument, allowing us to define whatever we want to happen whenever the user runs the code
  useSandpackRun(() => setFiles(sandpack.files));

  return (
    <>
      <SandpackPreview />
      <SandpackConsole />
    </>
  );
}

4. SandpackWithTests component

function SandpackWithTests({ files }) {
  const memoTests = useMemo(() => {
    return (
      <SandpackProvider
        options={{
          autoReload: false,
        }}
        template="vanilla"
        theme="dark"
        files={files} // Another Sandpack instance provided with the same files stored in the `files` state variable
      >
        <SandpackLayout>
          <SandpackTests watchMode={false} />
        </SandpackLayout>
      </SandpackProvider>
    );
  }, [files]); // Adding the `files` state variable as a dependency ensures that this Sandpack instance is always updated with the latest files (whenever the user runs the code)

  return memoTests;
}

5. Custom hook: useSandpackRun

const useSandpackRun = (onRunCallback) => {
  useEffect(() => {
    // 1. Query the actual button that the user clicks. This is very important, because this is how we'll know when to update our `files` state
    const buttons = document.querySelectorAll(
      'button.sp-icon-standalone.sp-c-bxeRRt.sp-c-gMfcns.sp-c-dEbKhQ.sp-c-bcibQq.sp-button[type="button"]'
    );

    // I checked that Sandpack has buttons with the same classes queried above
    // So, I wanted to make sure we are querying the right button by adding extra checks.
    // This is all based on the HTML I found by inspecting (see the image later in this comment).
    const runButton = Array.from(buttons).find((button) => {
      const span = button.querySelector("span");
      const svgTitle = button.querySelector("svg title");
      return (
        span &&
        span.textContent.trim() === "Run" &&
        svgTitle &&
        svgTitle.textContent.trim() === "Run sandbox"
      );
    });

    // 2. Create an event listener for triggering a callback function, whenever the button is clicked
    const handleButtonClick = () => {
      // note: We are triggering whatever argument is passed to the `onRunCallback` parameter
      // This way, we can define the callback whenever we're using this hook - the hook will take care of the rest.
      onRunCallback();
    };

    if (runButton && runButton instanceof HTMLElement) {
      runButton.addEventListener("click", handleButtonClick);
    }

    return () => {
      if (runButton && runButton instanceof HTMLElement) {
        runButton.removeEventListener("click", handleButtonClick);
      }
    };
  }, [onRunCallback]); // 3. Ensure that the event listener is attached whenever the `onRunCallback` changes
};

And there you have it! I hope this approach is a good enough work around for your projects. It was really a tough one to crack, since I couldn't find any documentation. Thankfully, this approach worked for me at least. I'm so happy to finally continue my projects with Sandpack, because it really is an awesome library.

Check out the resources section to learn more about this approach and try out the sandbox I created.


Resources

📹 Video example with Vanilla template

👨🏾‍💻 CodeSandbox (The sandbox includes "initial file"-setups for both Vanilla and React templates. Remember to update the template props of each Sandpack instance accordingly.)