Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation #239

Merged
merged 32 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
81329ff
Cargo.lock
mobile-bungalow Oct 30, 2024
9c5c451
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 6, 2024
b564457
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 14, 2024
f242756
8 failures, removed pointer chasing
mobile-bungalow Nov 16, 2024
cb8fdfc
lints
mobile-bungalow Nov 16, 2024
97b60eb
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 16, 2024
ef4094f
7 failing, do not newrender in conversion
mobile-bungalow Nov 18, 2024
8c653ca
clippy
mobile-bungalow Nov 18, 2024
cbac04c
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 18, 2024
bac2faa
first failing navigation test
mobile-bungalow Nov 18, 2024
320308f
nav files
mobile-bungalow Nov 18, 2024
0be4f69
first test passing
mobile-bungalow Nov 18, 2024
5a95faf
tests passing
mobile-bungalow Nov 19, 2024
0671dfd
remove unnecessary state object
mobile-bungalow Nov 19, 2024
63b53d4
all traversal logic implemented
mobile-bungalow Nov 19, 2024
123caed
tests passing
mobile-bungalow Nov 20, 2024
df9df11
more thorough tests
mobile-bungalow Nov 20, 2024
039c008
typo
mobile-bungalow Nov 20, 2024
cd8bede
clippy
mobile-bungalow Nov 20, 2024
d345c6f
lints
mobile-bungalow Nov 20, 2024
64b9556
clippy passing, api's wrapped
mobile-bungalow Nov 20, 2024
6b71560
start rollback state
mobile-bungalow Nov 21, 2024
cedaec0
rollback
mobile-bungalow Nov 21, 2024
6e2f3e0
state rollback works, add the begginning of the actual network code
mobile-bungalow Nov 21, 2024
8bf4464
actually changes page
mobile-bungalow Nov 22, 2024
4260c7c
add session data
mobile-bungalow Nov 22, 2024
cb70a14
swift tests
mobile-bungalow Nov 22, 2024
994292c
simple use case kotlin and swift
mobile-bungalow Nov 22, 2024
0349d25
remove generated files
mobile-bungalow Nov 22, 2024
10dc96f
remove short circuiting nav on failure
mobile-bungalow Nov 25, 2024
571b3d2
code review, remove excessive indirection, bubble errors, try lock
mobile-bungalow Nov 26, 2024
991340b
add lock macro
mobile-bungalow Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.phoenixframework.liveviewnative.core.Document
import org.phoenixframework.liveviewnative.core.DocumentChangeHandler
import org.phoenixframework.liveviewnative.core.LiveFile
import org.phoenixframework.liveviewnative.core.LiveSocket
import org.phoenixframework.liveviewnative.core.NavOptions
import org.phoenixframework.liveviewnative.core.NodeData
import org.phoenixframework.liveviewnative.core.NodeRef

Expand Down Expand Up @@ -245,4 +246,50 @@ class DocumentTest {
</Column>"""
assertEquals(expected, rendered)
}

@Test
fun basic_nav_flow() = runTest {
val host = "127.0.0.1:4001"
val url = "http://$host/nav/first_page"

val liveSocket = LiveSocket.connect(url, "jetpack", null)
val liveChannel = liveSocket.joinLiveviewChannel(null, null)
val doc = liveChannel.document()

val expectedFirstDoc =
"""
<Box size="fill" background="system-blue">
<Text align="Center">
first_page
<Link destination="/nav/next">
<Text class="bold">Next</Text>
</Link>
</Text>
</Box>
""".trimIndent()

val exp = Document.parse(expectedFirstDoc)
assertEquals(exp.render(), doc.render())

val secondUrl = "http://$host/nav/second_page"
liveSocket.navigate(secondUrl, NavOptions())

val secondChannel = liveSocket.joinLiveviewChannel(null, null)
val secondDoc = secondChannel.document()

val expectedSecondDoc =
"""
<Box size="fill" background="system-blue">
<Text align="Center">
second_page
<Link destination="/nav/next">
<Text class="bold">Next</Text>
</Link>
</Text>
</Box>
""".trimIndent()

val secondExp = Document.parse(expectedSecondDoc)
assertEquals(secondExp.render(), secondDoc.render())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,55 @@ final class LiveViewNativeCoreUploadTests: XCTestCase {
try await live_channel.uploadFile(live_file)
}
}

// Test basic navigation flow with LiveSocket
func testBasicNavFlow() async throws {
let url = "http://127.0.0.1:4001/nav/first_page"
let secondUrl = "http://127.0.0.1:4001/nav/second_page"

let liveSocket = try await LiveSocket(url, "swiftui", .none)
let liveChannel = try await liveSocket.joinLiveviewChannel(.none, .none)

let doc = liveChannel.document()

let expectedFirstDoc = """
<Group id="flash-group" />
<VStack>
<Text>
first_page
</Text>
<NavigationLink id="Next" destination="/nav/next">
<Text>
NEXT
</Text>
</NavigationLink>
</VStack>
"""

let exp = try Document.parse(expectedFirstDoc)

XCTAssertEqual(doc.render(), exp.render())

let _ = try await liveSocket.navigate(secondUrl, NavOptions())

let secondChannel = try await liveSocket.joinLiveviewChannel(.none, .none)
let secondDoc = secondChannel.document()

let expectedSecondDoc = """
<Group id="flash-group" />
<VStack>
<Text>
second_page
</Text>
<NavigationLink id="Next" destination="/nav/next">
<Text>
NEXT
</Text>
</NavigationLink>
</VStack>
"""

let secondExp = try Document.parse(expectedSecondDoc)

XCTAssertEqual(secondDoc.render(), secondExp.render())
}
50 changes: 50 additions & 0 deletions crates/core/src/dom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub struct Document {
/// A count of the number of uploads, the server expects each upload to have an ascending unique ID.
upload_ct: u64,
}

impl fmt::Debug for Document {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand All @@ -95,6 +96,7 @@ impl fmt::Debug for Document {
write!(f, "{:?}", &dot)
}
}

impl fmt::Display for Document {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -559,6 +561,54 @@ impl Document {
editor.finish();
Ok(results)
}

/// Returns the CSRF token if it is present in the page.
pub fn get_csrf_token(&self) -> Option<String> {
// HTML responses have
// <meta name="csrf-token"
// content="PBkccxQnXREEHjJhOksCJ1cVESUiRgtBYZxJSKpAEMJ0tfivopcul5Eq">
let meta_csrf_token: Option<String> = self
.select(Selector::Tag(ElementName {
namespace: None,
name: "meta".into(),
}))
.map(|node_ref| self.get(node_ref))
// We need the node of the element with a "name" attribute that equals "csrf-token"
.filter(|node| {
node.attributes()
.iter()
.filter(|attr| {
attr.name.name == *"name" && attr.value == Some("csrf-token".to_string())
})
.last()
.is_some()
})
// We now need the "content" value
.map(|node| {
node.attributes()
.iter()
.filter(|attr| attr.name.name == *"content")
.map(|attr| attr.value.clone())
.last()
.flatten()
})
.last()
.flatten();

log::debug!("META CSRF TOKEN: {meta_csrf_token:#?}");

// LiveView Native responses have:
// <csrf-token value="CgpDGHsSYUUxHxdQDSVVc1dmchgRYhMUXlqANTR3uQBdzHmK5R9mW5wu" />
self.select(Selector::Tag(ElementName {
namespace: None,
name: "csrf-token".into(),
}))
.last()
.map(|node_ref| self.get(node_ref))
.and_then(|node| node.attributes().first().map(|attr| attr.value.clone()))
.flatten()
.or(meta_csrf_token)
}
}

#[repr(C)]
Expand Down
10 changes: 5 additions & 5 deletions crates/core/src/live_socket/channel.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use std::{sync::Arc, time::Duration};

use log::{debug, error};
use phoenix_channels_client::{Channel, Event, Number, Payload, Socket, Topic, JSON};

use super::{LiveSocketError, UploadConfig, UploadError};
use crate::{
diff::fragment::{Root, RootDiff},
Expand All @@ -12,6 +9,8 @@ use crate::{
},
parser::parse,
};
use log::{debug, error};
use phoenix_channels_client::{Channel, Event, Number, Payload, Socket, Topic, JSON};

#[derive(uniffi::Object)]
pub struct LiveChannel {
Expand Down Expand Up @@ -60,6 +59,7 @@ impl LiveFile {

// For non FFI functions
impl LiveChannel {
/// Retrieves the initial document received upon joining the channel.
pub fn join_document(&self) -> Result<Document, LiveSocketError> {
let new_root = match self.join_payload {
Payload::JSONPayload {
Expand Down Expand Up @@ -137,8 +137,8 @@ impl LiveChannel {
Ok(upload_id)
}

// Blocks indefinitely, processing changes to the document using the user provided callback
// In `set_event_handler`
/// Blocks indefinitely, processing changes to the document using the user provided callback
/// In `set_event_handler`
pub async fn merge_diffs(&self) -> Result<(), LiveSocketError> {
// TODO: This should probably take the event closure to send changes back to swift/kotlin
let document = self.document.clone();
Expand Down
5 changes: 5 additions & 0 deletions crates/core/src/live_socket/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use crate::{

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum LiveSocketError {
#[error("Error disconnecting")]
DisconnectionError,
#[error("Navigation Impossible")]
NavigationImpossible,
#[error("Expected Json Payload, Was Binary")]
PayloadNotJson,
#[error("Could Not Parse Mime - {error}")]
Expand All @@ -30,6 +34,7 @@ pub enum LiveSocketError {
},
#[error("JSON Deserialization - {error}")]
JSONDeserialization { error: String },

#[error("CSFR Token Missing from DOM!")]
CSFRTokenMissing,

Expand Down
3 changes: 2 additions & 1 deletion crates/core/src/live_socket/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
mod channel;
mod error;
mod navigation;
mod socket;

#[cfg(test)]
mod tests;

pub use channel::LiveChannel;
use error::{LiveSocketError, UploadError};
pub use error::{LiveSocketError, UploadError};
pub use socket::LiveSocket;

pub struct UploadConfig {
Expand Down
Loading