Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenDE committed Feb 24, 2024
1 parent 0dbe35f commit afefc8d
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 3 deletions.
7 changes: 4 additions & 3 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
end

# these routes use a custom layout and therefore cannot be in the live_session
scope "/issues" do
scope "/issues", Phoenix.LiveViewTest.E2E do
pipe_through(:browser)

live "/3047/a", Phoenix.LiveViewTest.E2E.Issue3047ALive
live "/3047/b", Phoenix.LiveViewTest.E2E.Issue3047BLive
live "/2965", Issue2965Live
live "/3047/a", Issue3047ALive
live "/3047/b", Issue3047BLive
end
end

Expand Down
30 changes: 30 additions & 0 deletions test/e2e/tests/issues/2965.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { test, expect } = require("@playwright/test");
const { syncLV } = require("../../utils");
const { randomBytes } = require("crypto");

test("can upload files with custom chunk hook", async ({ page }) => {
await page.goto("/issues/2965");
await syncLV(page);

const files = [];
for (let i = 1; i <= 20; i++) {
files.push({
name: `file${i}.txt`,
mimeType: "text/plain",
// random 100 kb
buffer: randomBytes(100 * 1024),
});
}

await page.locator("#fileinput").setInputFiles(files);
await syncLV(page);

// wait for uploads to finish
for (let i = 0; i < 20; i++) {
const row = page.locator(`tbody tr`).nth(i);
await expect(row).toContainText(`file${i + 1}.txt`);
await expect(row.locator("progress")).toHaveAttribute("value", "100");
}

// all uploads are finished!
});
30 changes: 30 additions & 0 deletions test/e2e/tests/uploads.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,33 @@ test("auto upload", async ({ page }) => {

await expect(page.locator("ul li")).toBeVisible();
});

test("issue 3115 - cancelled upload is not re-added", async ({ page }) => {
await page.goto("/upload");
await syncLV(page);

await page.locator("#upload-form input").setInputFiles([
{
name: "file.txt",
mimeType: "text/plain",
buffer: Buffer.from("this is a test")
}
]);
await syncLV(page);
// cancel the file
await page.getByLabel("cancel").click();

// add other file
await page.locator("#upload-form input").setInputFiles([
{
name: "file.md",
mimeType: "text/markdown",
buffer: Buffer.from("## this is a markdown file")
}
]);
await syncLV(page);
await page.getByRole("button", { name: "Upload" }).click();

// we should see one uploaded file in the list
await expect(page.locator("ul li")).toHaveCount(1);
});
233 changes: 233 additions & 0 deletions test/support/e2e/issues/issue_2965.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
defmodule Phoenix.LiveViewTest.E2E.Issue2965Live do
use Phoenix.LiveView, layout: {__MODULE__, :live}

defmodule NoOpWriter do
@behaviour Phoenix.LiveView.UploadWriter

@impl true
def init(_opts) do
{:ok, nil}
end

@impl true
def meta(state), do: state

@impl true
def write_chunk(_data, state) do
Process.sleep((:rand.uniform() * 200) |> ceil())
{:ok, state}
end

def close(_state, :cancel) do
{:ok, :aborted}
end

@impl true
def close(_state, :done) do
{:ok, %{}}
end
end

def mount(_params, _session, socket) do
socket =
socket
|> allow_upload(:files,
accept: :any,
max_entries: 1500,
# minimum 5 mb for multipart
chunk_size: 5 * 1_024 * 1_024,
max_file_size: 10_000_000_000,
auto_upload: true,
writer: &noop_writer/3,
progress: &handle_progress/3
)
|> assign(:form, to_form(%{}))

{:ok, socket}
end

def render("live.html", assigns) do
~H"""
<meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />
<script src="/assets/phoenix/phoenix.min.js">
</script>
<script src="/assets/phoenix_live_view/phoenix_live_view.js">
</script>
<script>
const QueuedUploaderHook = {
async mounted() {
const maxConcurrency = this.el.dataset.maxConcurrency || 3;
let filesRemaining = [];
this.el.addEventListener("input", async (event) => {
event.preventDefault()
if (event.target instanceof HTMLInputElement) {
const files_html = event.target.files;
if (files_html) {
const rawFiles = Array.from(files_html);
const fileNames = rawFiles.map((f) => {
return f.name;
});
this.pushEvent("upload_scrub_list", { file_names: fileNames }, ({ deduped_filenames }, ref) => {
const files = rawFiles.filter((f) => {
return deduped_filenames.includes(f.name);
});
filesRemaining = files;
const firstFiles = files.slice(0, maxConcurrency);
this.upload("files", firstFiles);
filesRemaining.splice(0, maxConcurrency);
});
}
}
});
this.handleEvent("upload_send_next_file", () => {
if (filesRemaining.length > 0) {
const nextFile = filesRemaining.shift();
if (nextFile != undefined) {
this.upload("files", [nextFile]);
}
} else {
console.log("Done uploading, noop!");
}
});
}
};
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {
params: {_csrf_token: csrfToken},
hooks: {QueuedUploaderHook}
})
liveSocket.connect()
</script>
<%= @inner_content %>
"""
end

def render(assigns) do
~H"""
<main>
<h1>Uploader reproduction</h1>
<.form for={@form} phx-submit="save" phx-change="validate">
<%!-- use phx-drop-target with the upload ref to enable file drag and drop --%>
<%!-- phx-drop-target={@uploads.files.ref} --%>
<section>
<.live_file_input upload={@uploads.files} style="display: none;" />
<input
id="fileinput"
type="file"
multiple
phx-hook="QueuedUploaderHook"
disabled={file_picker_disabled?(@uploads)}
/>
<h2 :if={length(@uploads.files.entries) > 0}>Currently uploading files</h2>
<div>
<table>
<!-- head -->
<thead>
<tr>
<th>File Name</th>
<th>Progress</th>
<th>Cancel</th>
<th>Errors</th>
</tr>
</thead>
<tbody>
<%= for entry <- uploads_in_progress(@uploads) do %>
<tr>
<td><%= entry.client_name %></td>
<td>
<progress value={entry.progress} max="100">
<%= entry.progress %>%
</progress>
</td>
<td>
<button
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
>
<span>&times;</span>
</button>
</td>
<td>
<%= for err <- upload_errors(@uploads.files, entry) do %>
<p style="color: red;"><%= error_to_string(err) %></p>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%!-- Phoenix.Component.upload_errors/1 returns a list of error atoms --%>
<%= for err <- upload_errors(@uploads.files) do %>
<p style="text-red"><%= error_to_string(err) %></p>
<% end %>
</section>
</.form>
</main>
"""
end

def handle_progress(:files, entry, socket) do
if entry.done? do
{:noreply, push_event(socket, "upload_send_next_file", %{})}
else
{:noreply, socket}
end
end

# This dedupes against s3, just doing a no-op here to preserve the original uploader js code
def handle_event(
"upload_scrub_list",
%{"file_names" => file_names},
socket
) do
{:reply, %{deduped_filenames: file_names}, socket}
end

def handle_event("validate", _params, socket) do
{:noreply, socket}
end

def handle_event("cancel-upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :files, ref)}
end

def handle_event("save", _params, socket) do
{:noreply, socket}
end

def error_to_string(:too_large), do: "Too large"
def error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
def error_to_string(:s3_error), do: "Error on writing to cloudflare"

def error_to_string(_unknown) do
"unknown error"
end

## Helpers

defp file_picker_disabled?(uploads) do
Enum.any?(uploads.files.entries, fn e -> !e.done? end)
end

defp noop_writer(_name, %Phoenix.LiveView.UploadEntry{} = entry, _socket) do
{
__MODULE__.NoOpWriter,
provider: :r2, name: entry.client_name
}
end

defp uploads_in_progress(uploads) do
uploads.files.entries
end
end

0 comments on commit afefc8d

Please sign in to comment.